using System.Net;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Predictalytics.Hosting;

/// <summary>
/// Failed Deployment Center call. <see cref="Code"/> is the stable, machine readable
/// error code from the API envelope ("unauthorized", "rate_limited", ...) — react to it,
/// not to the message text.
/// </summary>
public sealed class DcApiException : Exception
{
    public DcApiException(HttpStatusCode statusCode, string? code, string body)
        : base($"Deployment Center HTTP {(int)statusCode}{(code is null ? "" : $" ({code})")}: {Shorten(body)}")
    {
        StatusCode = statusCode;
        Code = code;
    }

    public HttpStatusCode StatusCode { get; }
    public string? Code { get; }

    /// <summary>True for errors that repeating the same call cannot fix (wrong/missing token).</summary>
    public bool IsPermanent =>
        StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden ||
        Code is "unauthorized" or "project_forbidden";

    private static string Shorten(string body) =>
        body.Length <= 300 ? body : body[..300] + "…";
}

/// <summary>
/// Minimal JSON client for the Deployment Center API. Used by the heartbeat and the error
/// stream; the license and update modules bring their own client (Deploymentcenter.Client).
/// </summary>
public sealed class DcApiClient : IDisposable
{
    private static readonly JsonSerializerOptions JsonOptions = new()
    {
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
    };

    private readonly HttpClient _http;
    private readonly string _token;

    public DcApiClient(string token, TimeSpan? timeout = null)
    {
        _token = token ?? "";
        _http = new HttpClient { Timeout = timeout ?? TimeSpan.FromSeconds(10) };
    }

    public async Task<string> PostJsonAsync(string path, object payload, CancellationToken ct = default)
    {
        using var request = new HttpRequestMessage(HttpMethod.Post, DcConfig.BaseUrl + path);
        if (!string.IsNullOrWhiteSpace(_token))
        {
            request.Headers.TryAddWithoutValidation("Authorization", $"Bearer {_token}");
        }
        request.Content = new StringContent(
            JsonSerializer.Serialize(payload, JsonOptions), Encoding.UTF8, "application/json");

        using var response = await _http.SendAsync(request, ct).ConfigureAwait(false);
        var body = await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false);

        if (!response.IsSuccessStatusCode)
        {
            throw new DcApiException(response.StatusCode, ExtractErrorCode(body), body);
        }
        return body;
    }

    /// <summary>Reads error.code out of {"status":"error","error":{"code":"…"}}.</summary>
    private static string? ExtractErrorCode(string body)
    {
        try
        {
            using var doc = JsonDocument.Parse(body);
            if (doc.RootElement.ValueKind == JsonValueKind.Object &&
                doc.RootElement.TryGetProperty("error", out var error) &&
                error.ValueKind == JsonValueKind.Object &&
                error.TryGetProperty("code", out var code))
            {
                return code.GetString();
            }
        }
        catch (JsonException)
        {
            // Not every error path answers with the envelope (e.g. a proxy returning HTML).
        }
        return null;
    }

    public void Dispose() => _http.Dispose();
}
