using System.Collections.Generic;
using System.Reflection;
using System.Runtime.Serialization;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;

namespace StellaOps.Vexer.Core;

public static class VexCanonicalJsonSerializer
{
    private static readonly JsonSerializerOptions CompactOptions = CreateOptions(writeIndented: false);
    private static readonly JsonSerializerOptions PrettyOptions = CreateOptions(writeIndented: true);

    private static readonly IReadOnlyDictionary<Type, string[]> PropertyOrderOverrides = new Dictionary<Type, string[]>
    {
        {
            typeof(VexProvider),
            new[]
            {
                "id",
                "displayName",
                "kind",
                "baseUris",
                "discovery",
                "trust",
                "enabled",
            }
        },
        {
            typeof(VexProviderDiscovery),
            new[]
            {
                "wellKnownMetadata",
                "rolIeService",
            }
        },
        {
            typeof(VexProviderTrust),
            new[]
            {
                "weight",
                "cosign",
                "pgpFingerprints",
            }
        },
        {
            typeof(VexCosignTrust),
            new[]
            {
                "issuer",
                "identityPattern",
            }
        },
        {
            typeof(VexClaim),
            new[]
            {
                "vulnerabilityId",
                "providerId",
                "product",
                "status",
                "justification",
                "detail",
                "document",
                "firstSeen",
                "lastSeen",
                "confidence",
                "additionalMetadata",
            }
        },
        {
            typeof(VexProduct),
            new[]
            {
                "key",
                "name",
                "version",
                "purl",
                "cpe",
                "componentIdentifiers",
            }
        },
        {
            typeof(VexClaimDocument),
            new[]
            {
                "format",
                "digest",
                "sourceUri",
                "revision",
                "signature",
            }
        },
        {
            typeof(VexSignatureMetadata),
            new[]
            {
                "type",
                "subject",
                "issuer",
                "keyId",
                "verifiedAt",
                "transparencyLogReference",
            }
        },
        {
            typeof(VexConfidence),
            new[]
            {
                "level",
                "score",
                "method",
            }
        },
        {
            typeof(VexConsensus),
            new[]
            {
                "vulnerabilityId",
                "product",
                "status",
                "calculatedAt",
                "sources",
                "conflicts",
                "policyVersion",
                "summary",
            }
        },
        {
            typeof(VexConsensusSource),
            new[]
            {
                "providerId",
                "status",
                "documentDigest",
                "weight",
                "justification",
                "detail",
                "confidence",
            }
        },
        {
            typeof(VexConsensusConflict),
            new[]
            {
                "providerId",
                "status",
                "documentDigest",
                "justification",
                "detail",
                "reason",
            }
        },
        {
            typeof(VexConnectorSettings),
            new[]
            {
                "values",
            }
        },
        {
            typeof(VexConnectorContext),
            new[]
            {
                "since",
                "settings",
                "rawSink",
                "signatureVerifier",
                "normalizers",
                "services",
            }
        },
        {
            typeof(VexRawDocument),
            new[]
            {
                "documentId",
                "providerId",
                "format",
                "sourceUri",
                "retrievedAt",
                "digest",
                "content",
                "metadata",
            }
        },
        {
            typeof(VexClaimBatch),
            new[]
            {
                "source",
                "claims",
                "diagnostics",
            }
        },
        {
            typeof(VexExportManifest),
            new[]
            {
                "exportId",
                "querySignature",
                "format",
                "createdAt",
                "artifact",
                "claimCount",
                "fromCache",
                "sourceProviders",
                "consensusRevision",
                "attestation",
                "sizeBytes",
            }
        },
        {
            typeof(VexContentAddress),
            new[]
            {
                "algorithm",
                "digest",
            }
        },
        {
            typeof(VexAttestationMetadata),
            new[]
            {
                "predicateType",
                "rekor",
                "envelopeDigest",
                "signedAt",
            }
        },
        {
            typeof(VexRekorReference),
            new[]
            {
                "apiVersion",
                "location",
                "logIndex",
                "inclusionProofUri",
            }
        },
        {
            typeof(VexQuerySignature),
            new[]
            {
                "value",
            }
        },
        {
            typeof(VexQuery),
            new[]
            {
                "filters",
                "sort",
                "limit",
                "offset",
                "view",
            }
        },
        {
            typeof(VexQueryFilter),
            new[]
            {
                "key",
                "value",
            }
        },
        {
            typeof(VexQuerySort),
            new[]
            {
                "field",
                "descending",
            }
        },
        {
            typeof(VexExportRequest),
            new[]
            {
                "query",
                "consensus",
                "claims",
                "generatedAt",
            }
        },
        {
            typeof(VexExportResult),
            new[]
            {
                "digest",
                "bytesWritten",
                "metadata",
            }
        },
        {
            typeof(VexAttestationRequest),
            new[]
            {
                "querySignature",
                "artifact",
                "format",
                "createdAt",
                "metadata",
            }
        },
        {
            typeof(VexAttestationResponse),
            new[]
            {
                "attestation",
                "diagnostics",
            }
        },
        {
            typeof(VexAttestationVerification),
            new[]
            {
                "isValid",
                "diagnostics",
            }
        },
    };

    public static string Serialize<T>(T value)
        => JsonSerializer.Serialize(value, CompactOptions);

    public static string SerializeIndented<T>(T value)
        => JsonSerializer.Serialize(value, PrettyOptions);

    public static T Deserialize<T>(string json)
        => JsonSerializer.Deserialize<T>(json, PrettyOptions)
            ?? throw new InvalidOperationException($"Unable to deserialize type {typeof(T).Name}.");

    private static JsonSerializerOptions CreateOptions(bool writeIndented)
    {
        var options = new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
            DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
            DefaultIgnoreCondition = JsonIgnoreCondition.Never,
            WriteIndented = writeIndented,
            Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
        };

        var baselineResolver = options.TypeInfoResolver ?? new DefaultJsonTypeInfoResolver();
        options.TypeInfoResolver = new DeterministicTypeInfoResolver(baselineResolver);
        options.Converters.Add(new EnumMemberJsonConverterFactory());
        return options;
    }

    private sealed class DeterministicTypeInfoResolver : IJsonTypeInfoResolver
    {
        private readonly IJsonTypeInfoResolver _inner;

        public DeterministicTypeInfoResolver(IJsonTypeInfoResolver inner)
        {
            _inner = inner ?? throw new ArgumentNullException(nameof(inner));
        }

        public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options)
        {
            var info = _inner.GetTypeInfo(type, options);
            if (info is null)
            {
                return null;
            }

            if (info.Kind is JsonTypeInfoKind.Object && info.Properties is { Count: > 1 })
            {
                var ordered = info.Properties
                    .OrderBy(property => GetPropertyOrder(type, property.Name))
                    .ThenBy(property => property.Name, StringComparer.Ordinal)
                    .ToArray();

                info.Properties.Clear();
                foreach (var property in ordered)
                {
                    info.Properties.Add(property);
                }
            }

            return info;
        }

        private static int GetPropertyOrder(Type type, string propertyName)
        {
            if (PropertyOrderOverrides.TryGetValue(type, out var order) &&
                Array.IndexOf(order, propertyName) is var index &&
                index >= 0)
            {
                return index;
            }

            return int.MaxValue;
        }
    }

    private sealed class EnumMemberJsonConverterFactory : JsonConverterFactory
    {
        public override bool CanConvert(Type typeToConvert)
        {
            var type = Nullable.GetUnderlyingType(typeToConvert) ?? typeToConvert;
            return type.IsEnum;
        }

        public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
        {
            var underlying = Nullable.GetUnderlyingType(typeToConvert);
            if (underlying is not null)
            {
                var nullableConverterType = typeof(NullableEnumMemberJsonConverter<>).MakeGenericType(underlying);
                return (JsonConverter)Activator.CreateInstance(nullableConverterType)!;
            }

            var converterType = typeof(EnumMemberJsonConverter<>).MakeGenericType(typeToConvert);
            return (JsonConverter)Activator.CreateInstance(converterType)!;
        }

        private sealed class EnumMemberJsonConverter<T> : JsonConverter<T>
            where T : struct, Enum
        {
            private readonly Dictionary<string, T> _nameToValue;
            private readonly Dictionary<T, string> _valueToName;

            public EnumMemberJsonConverter()
            {
                _nameToValue = new Dictionary<string, T>(StringComparer.Ordinal);
                _valueToName = new Dictionary<T, string>();
                foreach (var value in Enum.GetValues<T>())
                {
                    var name = value.ToString();
                    var enumMember = typeof(T).GetField(name)!.GetCustomAttribute<EnumMemberAttribute>();
                    var text = enumMember?.Value ?? name;
                    _nameToValue[text] = value;
                    _valueToName[value] = text;
                }
            }

            public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
            {
                if (reader.TokenType != JsonTokenType.String)
                {
                    throw new JsonException($"Unexpected token '{reader.TokenType}' when parsing enum '{typeof(T).Name}'.");
                }

                var text = reader.GetString();
                if (text is null || !_nameToValue.TryGetValue(text, out var value))
                {
                    throw new JsonException($"Value '{text}' is not defined for enum '{typeof(T).Name}'.");
                }

                return value;
            }

            public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
            {
                if (!_valueToName.TryGetValue(value, out var text))
                {
                    throw new JsonException($"Value '{value}' is not defined for enum '{typeof(T).Name}'.");
                }

                writer.WriteStringValue(text);
            }
        }

        private sealed class NullableEnumMemberJsonConverter<T> : JsonConverter<T?>
            where T : struct, Enum
        {
            private readonly EnumMemberJsonConverter<T> _inner = new();

            public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
            {
                if (reader.TokenType == JsonTokenType.Null)
                {
                    return null;
                }

                return _inner.Read(ref reader, typeof(T), options);
            }

            public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
            {
                if (value is null)
                {
                    writer.WriteNullValue();
                    return;
                }

                _inner.Write(writer, value.Value, options);
            }
        }
    }
}
