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

namespace ClawdDotNet.Core.Deploymentcenter;

/// <summary>
/// Der gemeinsame Unterbau für alle Deploymentcenter-Module (Watchdog, Fehler-Stream,
/// Bugtracker, Token-Provisionierung).
///
/// <para>Alle JSON-Endpunkte antworten einheitlich mit einem Umschlag —
/// <c>{"status":"success",…}</c> bzw. <c>{"status":"error","error":{"code":…}}</c>. Der
/// <c>code</c> ist stabil und für Programme gedacht, die <c>message</c> für Menschen.
/// Diese Klasse packt den Umschlag aus und macht aus einem Fehler eine
/// <see cref="DeploymentcenterException"/> mit dem Code daran.</para>
///
/// <para><b>Ausnahme:</b> Die Lizenz-Endpunkte tragen diesen Umschlag bewusst
/// <em>nicht</em> — dort steht im Feld <c>status</c> der Lizenzzustand. Sie werden
/// deshalb nicht hierüber, sondern über <c>Deploymentcenter.Client</c> angesprochen.</para>
/// </summary>
public sealed class DeploymentcenterApi : IDisposable
{
    private static readonly JsonSerializerOptions JsonOpts = new(JsonSerializerDefaults.Web);

    private readonly HttpClient _http;
    private readonly bool _ownsHttp;
    private readonly string _token;

    /// <summary>Die Basis-URL ohne abschließenden Schrägstrich.</summary>
    public string BaseUrl { get; }

    /// <param name="baseUrl">Basis-URL des Deploymentcenters, etwa <c>https://dc.mhdf.de</c>.</param>
    /// <param name="token">Token mit den nötigen Rechten. Geht als <c>Authorization: Bearer</c> mit.</param>
    /// <param name="httpClient">Nur für Tests — sonst wird ein eigener mit Zeitgrenze erstellt.</param>
    public DeploymentcenterApi(string baseUrl, string token, HttpClient? httpClient = null)
    {
        if (string.IsNullOrWhiteSpace(baseUrl))
            throw new ArgumentException("Deploymentcenter-URL fehlt.", nameof(baseUrl));

        // Über eine ungesicherte Verbindung ginge das Token im Klartext. Ausnahme ist
        // nur der eigene Rechner — dort gibt es keine Strecke, auf der jemand mithören
        // könnte, und eine lokale Testinstallation hat selten ein Zertifikat.
        if (!IsAcceptableUrl(baseUrl))
        {
            throw new ArgumentException(
                "Deploymentcenter-URL muss mit https:// beginnen (Ausnahme: localhost).",
                nameof(baseUrl));
        }

        BaseUrl = baseUrl.TrimEnd('/');
        _token = token ?? throw new ArgumentNullException(nameof(token));

        _ownsHttp = httpClient is null;
        _http = httpClient ?? new HttpClient { Timeout = TimeSpan.FromSeconds(10) };
    }

    private static bool IsAcceptableUrl(string url)
    {
        if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
            return false;

        if (uri.Scheme == Uri.UriSchemeHttps)
            return true;

        return uri.Scheme == Uri.UriSchemeHttp && uri.IsLoopback;
    }

    public async Task<JsonElement> PostAsync(string path, object payload, CancellationToken ct)
    {
        using var request = new HttpRequestMessage(HttpMethod.Post, BaseUrl + path)
        {
            Content = new StringContent(
                JsonSerializer.Serialize(payload, JsonOpts), Encoding.UTF8, "application/json")
        };

        return await SendAsync(request, ct).ConfigureAwait(false);
    }

    public async Task<JsonElement> GetAsync(string path, CancellationToken ct)
    {
        using var request = new HttpRequestMessage(HttpMethod.Get, BaseUrl + path);
        return await SendAsync(request, ct).ConfigureAwait(false);
    }

    private async Task<JsonElement> SendAsync(HttpRequestMessage request, CancellationToken ct)
    {
        if (_token.Length > 0)
            request.Headers.TryAddWithoutValidation("Authorization", "Bearer " + _token);

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

        JsonElement root;
        try
        {
            // Geklont, weil das JsonDocument am Ende dieses Blocks freigegeben wird —
            // ein JsonElement daraus wäre danach nicht mehr lesbar.
            using var document = JsonDocument.Parse(string.IsNullOrWhiteSpace(body) ? "{}" : body);
            root = document.RootElement.Clone();
        }
        catch (JsonException)
        {
            throw new DeploymentcenterException(
                "invalid_response",
                $"Antwort war kein JSON (HTTP {(int)response.StatusCode}).",
                response.StatusCode);
        }

        if (IsErrorEnvelope(root, out var code, out var message))
            throw new DeploymentcenterException(code, message, response.StatusCode);

        if (!response.IsSuccessStatusCode)
        {
            throw new DeploymentcenterException(
                "http_error",
                $"Deploymentcenter antwortete HTTP {(int)response.StatusCode}.",
                response.StatusCode);
        }

        return root;
    }

    private static bool IsErrorEnvelope(JsonElement root, out string code, out string message)
    {
        code = "error";
        message = "Unbekannter Fehler.";

        if (root.ValueKind != JsonValueKind.Object)
            return false;

        if (!root.TryGetProperty("status", out var status)
            || status.ValueKind != JsonValueKind.String
            || !string.Equals(status.GetString(), "error", StringComparison.OrdinalIgnoreCase))
        {
            return false;
        }

        if (root.TryGetProperty("error", out var error) && error.ValueKind == JsonValueKind.Object)
        {
            if (error.TryGetProperty("code", out var c) && c.ValueKind == JsonValueKind.String)
                code = c.GetString() ?? code;

            if (error.TryGetProperty("message", out var m) && m.ValueKind == JsonValueKind.String)
                message = m.GetString() ?? message;
        }

        return true;
    }

    public void Dispose()
    {
        if (_ownsHttp)
            _http.Dispose();
    }
}

/// <summary>
/// Ein vom Deploymentcenter abgelehnter Aufruf. <see cref="Code"/> ist der stabile
/// Fehlercode aus dem Umschlag (<c>unauthorized</c>, <c>rate_limited</c>, …) — er ist
/// zum Auswerten gedacht, der Text nicht.
/// </summary>
public sealed class DeploymentcenterException(string code, string message, HttpStatusCode statusCode)
    : Exception($"{message} [{code}]")
{
    public string Code { get; } = code;

    public HttpStatusCode StatusCode { get; } = statusCode;

    /// <summary>Token fehlt, ist abgelaufen oder deckt das nötige Recht nicht ab.</summary>
    public bool IsAuthorizationProblem =>
        StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden;
}
