using System.Buffers;
using System.Net;
using System.Text.Json;

namespace Dada.Supervisor;

internal static class AmapProbe
{
    internal const int MaximumResponseBytes = 65_536;
    private const string ProviderHostname = "restapi.amap.com";
    private static readonly HttpClient Client = new(CreateHandler()) { Timeout = TimeSpan.FromSeconds(15) };

    internal static SocketsHttpHandler CreateHandler() => new()
    {
        AllowAutoRedirect = false,
        AutomaticDecompression = DecompressionMethods.None,
        ConnectTimeout = TimeSpan.FromSeconds(10),
        MaxConnectionsPerServer = 1,
    };

    internal static bool IsAllowedEndpoint(Uri endpoint) =>
        endpoint.Scheme == Uri.UriSchemeHttps
        && endpoint.Host.Equals(ProviderHostname, StringComparison.OrdinalIgnoreCase)
        && (endpoint.IsDefaultPort || endpoint.Port == 443)
        && string.IsNullOrEmpty(endpoint.UserInfo)
        && endpoint.AbsolutePath is "/v3/geocode/regeo" or "/v3/geocode/geo";

    internal static int Run(string? key)
    {
        if (string.IsNullOrWhiteSpace(key))
        {
            Write("amap_credentials_missing", false, 0, null, null);
            return 3;
        }

        var realCalls = 0;
        try
        {
            realCalls += 1;
            var reverse = Call("https://restapi.amap.com/v3/geocode/regeo?location=120.6994,27.9943&extensions=base", key).GetAwaiter().GetResult();
            realCalls += 1;
            var geocode = Call($"https://restapi.amap.com/v3/geocode/geo?address={Uri.EscapeDataString("北京市天安门")}", key).GetAwaiter().GetResult();
            var success = reverse.Status == "1" && geocode.Status == "1";
            Write(success ? "amap_probe_passed" : "amap_provider_rejected", success, realCalls, reverse.Status, geocode.Status);
            return success ? 0 : 3;
        }
        catch (TaskCanceledException)
        {
            Write("amap_probe_timeout", false, realCalls, null, null);
            return 3;
        }
        catch (HttpRequestException exception)
        {
            Write($"amap_probe_network_{exception.StatusCode?.ToString() ?? "error"}", false, realCalls, null, null);
            return 3;
        }
        catch (Exception exception) when (exception is JsonException or InvalidDataException)
        {
            Write("amap_probe_invalid_response", false, realCalls, null, null);
            return 3;
        }
    }

    private static async Task<ProbeResponse> Call(string endpoint, string key)
    {
        var requestUri = new Uri($"{endpoint}&key={Uri.EscapeDataString(key)}", UriKind.Absolute);
        if (!IsAllowedEndpoint(requestUri)) throw new InvalidDataException("amap_endpoint_rejected");
        using var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
        using var response = await Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
        response.EnsureSuccessStatusCode();
        using var document = await ReadBoundedJsonAsync(response.Content);
        var root = document.RootElement;
        return new ProbeResponse(root.GetProperty("status").GetString() ?? "", root.TryGetProperty("infocode", out var code) ? code.GetString() : null);
    }

    internal static async Task<JsonDocument> ReadBoundedJsonAsync(HttpContent content)
    {
        if (content.Headers.ContentLength is > MaximumResponseBytes) throw new InvalidDataException("amap_response_too_large");
        await using var stream = await content.ReadAsStreamAsync();
        using var buffered = new MemoryStream();
        var buffer = ArrayPool<byte>.Shared.Rent(4_096);
        try
        {
            var total = 0;
            while (true)
            {
                var read = await stream.ReadAsync(buffer);
                if (read == 0) break;
                total += read;
                if (total > MaximumResponseBytes) throw new InvalidDataException("amap_response_too_large");
                await buffered.WriteAsync(buffer.AsMemory(0, read));
            }
            buffered.Position = 0;
            return await JsonDocument.ParseAsync(buffered);
        }
        finally
        {
            Array.Clear(buffer);
            ArrayPool<byte>.Shared.Return(buffer);
        }
    }

    private static void Write(string code, bool success, int realCalls, string? reverseStatus, string? geocodeStatus) =>
        Console.WriteLine(JsonSerializer.Serialize(new { code, success, real_calls = realCalls, reverse_status = reverseStatus, geocode_status = geocodeStatus }));

    private sealed record ProbeResponse(string Status, string? InfoCode);
}
