using System.Text.RegularExpressions;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;

namespace DodoSSH.Api.Setup;

/// <summary>
/// OpenAPI document configuration.
/// </summary>
/// <remarks>
/// The generated document exists for third parties and a future CLI. It is emitted at
/// build time to <c>artifacts/openapi/v1.json</c> and diffed in CI so an unintended
/// contract change fails the pull request. The desktop client's actual contract is the
/// <c>DodoSSH.Contracts</c> assembly, guarded by PublicApiAnalyzers.
/// </remarks>
internal static partial class OpenApi
{
    internal const string DocumentName = "v1";

    internal static IServiceCollection AddDodoOpenApi(this IServiceCollection services)
    {
        services.AddOpenApi(
            DocumentName,
            options => options.AddOperationTransformer<RouteParameterTransformer>());

        // Added in M1, once there are endpoints to describe:
        //   - a document transformer contributing the OAuth2 authorizationCode + PKCE
        //     security scheme, so the document is usable from a generated client
        //   - a schema transformer mapping byte[] to {type: string, format: byte},
        //     since every ciphertext field crosses the wire as base64
        return services;
    }
}

/// <summary>
/// Declares the path parameters that the route template uses but no handler parameter binds.
/// </summary>
/// <remarks>
/// <para>
/// Endpoints that need a route value read it with <c>Route&lt;T&gt;("name")</c> rather than binding it
/// onto the request DTO, so nothing in the endpoint's signature mentions it and the generator emits
/// <c>/api/v1/vaults/{vaultId}/sync/pull</c> with an empty <c>parameters</c> list. A template expression
/// with no matching parameter is invalid OpenAPI, and no client generator can fill it in — which would
/// quietly make the document useless for the third parties it exists for.
/// </para>
/// <para>
/// The constraint survives in <c>ApiDescription.RelativePath</c> even though the document's path key
/// strips it, which is what makes the type recoverable rather than guessed.
/// </para>
/// </remarks>
internal sealed partial class RouteParameterTransformer : IOpenApiOperationTransformer
{
    /// <inheritdoc />
    public Task TransformAsync(
        OpenApiOperation operation,
        OpenApiOperationTransformerContext context,
        CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(operation);
        ArgumentNullException.ThrowIfNull(context);

        var template = context.Description.RelativePath;
        if (string.IsNullOrEmpty(template))
        {
            return Task.CompletedTask;
        }

        foreach (Match match in TemplateParameter().Matches(template))
        {
            var name = match.Groups["name"].Value;

            var alreadyDeclared = operation.Parameters?.Any(
                parameter => string.Equals(parameter.Name, name, StringComparison.OrdinalIgnoreCase));

            if (alreadyDeclared == true)
            {
                continue;
            }

            operation.Parameters ??= [];
            operation.Parameters.Add(new OpenApiParameter
            {
                Name = name,
                In = ParameterLocation.Path,

                // A path parameter is required by definition; the specification rejects any other value.
                Required = true,
                Schema = SchemaFor(match.Groups["constraint"].Value),
            });
        }

        return Task.CompletedTask;
    }

    /// <remarks>
    /// Only the constraints this API actually uses are mapped. An unrecognised one becomes a plain
    /// string, which is weaker than it could be but never wrong — the alternative, guessing, puts a type
    /// in a published contract that the server does not enforce.
    /// </remarks>
    private static OpenApiSchema SchemaFor(string constraint) => constraint switch
    {
        "guid" => new OpenApiSchema { Type = JsonSchemaType.String, Format = "uuid" },
        "int" => new OpenApiSchema { Type = JsonSchemaType.Integer, Format = "int32" },
        "long" => new OpenApiSchema { Type = JsonSchemaType.Integer, Format = "int64" },
        _ => new OpenApiSchema { Type = JsonSchemaType.String },
    };

    /// <summary>Matches <c>{name}</c> and <c>{name:constraint}</c>, ignoring catch-all and optional forms.</summary>
    /// <remarks>
    /// The timeout is there to satisfy MA0009 rather than because it can fire: the input is this
    /// server's own route table, read once at document generation, and never anything a caller sent.
    /// </remarks>
    [GeneratedRegex(
        @"\{(?<name>[A-Za-z_][A-Za-z0-9_]*)(?::(?<constraint>[^}]+))?\}",
        RegexOptions.None,
        matchTimeoutMilliseconds: 1000)]
    private static partial Regex TemplateParameter();
}
