using System.Reflection;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;
using Newtonsoft.Json.Serialization;

namespace ErsatzTV.Serialization;

/// <summary>
/// OpenAPI schema transformer that renames each schema property to the exact JSON key the runtime
/// Newtonsoft serializer (configured with <see cref="CustomContractResolver" />) would actually emit.
/// The OpenAPI document is generated from System.Text.Json metadata, whose camelCase naming can drift
/// from Newtonsoft's — e.g. STJ emits "fFmpegProfileId" while the MVC pipeline emits "ffmpegProfileId"
/// (the <see cref="CustomNamingStrategy" /> special case). Mirroring the real contract resolver keeps
/// the spec in lockstep with the wire format by construction, so future [JsonProperty] renames or
/// naming-strategy special cases cannot drift. See issue #198.
/// </summary>
public static class NewtonsoftSchemaNamingTransformer
{
    private static readonly CustomContractResolver ContractResolver = new();

    public static Task TransformAsync(
        OpenApiSchema schema,
        OpenApiSchemaTransformerContext context,
        CancellationToken cancellationToken)
    {
        if (schema.Properties is not { Count: > 0 } properties)
        {
            return Task.CompletedTask;
        }

        Type clrType = context.JsonTypeInfo.Type;
        if (ContractResolver.ResolveContract(clrType) is not JsonObjectContract contract)
        {
            return Task.CompletedTask;
        }

        // CLR member name -> Newtonsoft-emitted JSON property name.
        var memberToNewtonsoftName = new Dictionary<string, string>(StringComparer.Ordinal);
        foreach (JsonProperty jsonProperty in contract.Properties)
        {
            if (jsonProperty.UnderlyingName is { } underlyingName && jsonProperty.PropertyName is { } propertyName)
            {
                memberToNewtonsoftName[underlyingName] = propertyName;
            }
        }

        // Current schema key (STJ name) -> desired key (Newtonsoft name), only where they differ.
        var renames = new Dictionary<string, string>(StringComparer.Ordinal);
        foreach (var jsonPropertyInfo in context.JsonTypeInfo.Properties)
        {
            string schemaKey = jsonPropertyInfo.Name;
            if (!properties.ContainsKey(schemaKey))
            {
                continue;
            }

            if (jsonPropertyInfo.AttributeProvider is MemberInfo member &&
                memberToNewtonsoftName.TryGetValue(member.Name, out string newtonsoftName) &&
                !string.Equals(newtonsoftName, schemaKey, StringComparison.Ordinal))
            {
                renames[schemaKey] = newtonsoftName;
            }
        }

        if (renames.Count == 0)
        {
            return Task.CompletedTask;
        }

        // Rebuild Properties preserving insertion order. A rename colliding with an existing key
        // would silently drop a property from the spec — fail loudly instead (near-unreachable,
        // but the generator must never emit a lossy document).
        var renamedProperties = new Dictionary<string, IOpenApiSchema>(properties.Count, StringComparer.Ordinal);
        foreach ((string key, IOpenApiSchema value) in properties)
        {
            string finalKey = renames.TryGetValue(key, out string newKey) ? newKey : key;
            if (!renamedProperties.TryAdd(finalKey, value))
            {
                throw new InvalidOperationException(
                    $"OpenAPI schema property rename collision on '{clrType.FullName}': renaming '{key}' to '{finalKey}' would overwrite an existing property.");
            }
        }

        schema.Properties = renamedProperties;

        if (schema.Required is { Count: > 0 } required)
        {
            var renamedRequired = new System.Collections.Generic.HashSet<string>(required.Count, StringComparer.Ordinal);
            foreach (string key in required)
            {
                renamedRequired.Add(renames.TryGetValue(key, out string newKey) ? newKey : key);
            }

            schema.Required = renamedRequired;
        }

        return Task.CompletedTask;
    }
}
