// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.

using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;

using Elastic.Transport;

#if ELASTICSEARCH_SERVERLESS
namespace Elastic.Clients.Elasticsearch.Serverless.Serialization;
#else
namespace Elastic.Clients.Elasticsearch.Serialization;
#endif

/// <summary>
/// The built-in internal serializer that the <see cref="ElasticsearchClient"/> uses to serialize built in types.
/// </summary>
internal sealed class DefaultRequestResponseSerializer : SystemTextJsonSerializer
{
	private readonly IElasticsearchClientSettings _settings;

#if !NET8_0_OR_GREATER
	private readonly object _lock = new();
#endif

	public DefaultRequestResponseSerializer(IElasticsearchClientSettings settings) :
		base(new DefaultRequestResponseSerializerOptionsProvider(settings))
	{
		_settings = settings;

		LinkSettings(settings);
	}

	public override void Serialize<T>(T data, Stream writableStream,
		SerializationFormatting formatting = SerializationFormatting.None)
	{
		if (data is IStreamSerializable streamSerializable)
		{
			streamSerializable.Serialize(writableStream, _settings, SerializationFormatting.None);
			return;
		}

		base.Serialize(data, writableStream, formatting);
	}

	public override Task SerializeAsync<T>(T data, Stream stream,
		SerializationFormatting formatting = SerializationFormatting.None,
		CancellationToken cancellationToken = default)
	{
		if (data is IStreamSerializable streamSerializable)
			return streamSerializable.SerializeAsync(stream, _settings, SerializationFormatting.None);

		return base.SerializeAsync(data, stream, formatting, cancellationToken);
	}

	/// <summary>
	/// Links the <see cref="JsonSerializerOptions"/> of this serializer to the given <see cref="IElasticsearchClientSettings"/>.
	/// </summary>
	private void LinkSettings(IElasticsearchClientSettings settings)
	{
		var options = GetJsonSerializerOptions(SerializationFormatting.None);
		var indentedOptions = GetJsonSerializerOptions(SerializationFormatting.Indented);

#if NET8_0_OR_GREATER
		ElasticsearchClient.SettingsTable.TryAdd(options, settings);
		ElasticsearchClient.SettingsTable.TryAdd(indentedOptions, settings);
#else
		lock (_lock)
		{
			if (!ElasticsearchClient.SettingsTable.TryGetValue(options, out _))
			{
				ElasticsearchClient.SettingsTable.Add(options, settings);
			}

			if (!ElasticsearchClient.SettingsTable.TryGetValue(indentedOptions, out _))
			{
				ElasticsearchClient.SettingsTable.Add(indentedOptions, settings);
			}
		}
#endif
	}
}

/// <summary>
/// The options-provider for the built-in <see cref="DefaultRequestResponseSerializer"/>.
/// </summary>
internal sealed class DefaultRequestResponseSerializerOptionsProvider :
	TransportSerializerOptionsProvider
{
	internal DefaultRequestResponseSerializerOptionsProvider(IElasticsearchClientSettings settings) :
		base(CreateDefaultBuiltInConverters(settings), null, MutateOptions)
	{
	}

	private static IReadOnlyCollection<JsonConverter> CreateDefaultBuiltInConverters(IElasticsearchClientSettings settings) =>
	[
		new KeyValuePairConverterFactory(settings),
		new ObjectToInferredTypesConverter(),
		new SourceConverterFactory(settings),
		new SelfSerializableConverterFactory(settings),
		new SelfDeserializableConverterFactory(settings),
		new SelfTwoWaySerializableConverterFactory(settings),
		// Explicitly registered before `IsADictionaryConverterFactory` as we want this specialised converter to match
		new FieldValuesConverter(),
		new IsADictionaryConverterFactory(),
		new ResponseItemConverterFactory(),
		new DictionaryResponseConverterFactory(settings),
		new UnionConverter(),
		// TODO: Remove after https://github.com/elastic/elasticsearch-specification/issues/2238 is implemented
		new StringifiedLongConverter(),
		new StringifiedIntegerConverter(),
		new StringifiedBoolConverter()
	];

	private static void MutateOptions(JsonSerializerOptions options)
	{
		options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
		options.IncludeFields = true;
		options.NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals;
		options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
	}
}