using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;

namespace ZB.MOM.WW.ScadaBridge.CLI;

public class ManagementHttpClient : IDisposable
{
    private readonly HttpClient _httpClient;

    /// <summary>
    /// Default bound on the CONNECT phase only (30 s) — how long a TCP/TLS
    /// connection attempt to a black-holed management endpoint may hang before the
    /// call fails.
    ///
    /// <para>
    /// It is deliberately NOT an overall request timeout.
    /// <see cref="HttpClient.Timeout"/> caps the whole request/response, so setting
    /// it to any fixed value silently truncates every caller whose own per-call
    /// <c>TimeSpan timeout</c> argument is longer: the effective deadline becomes
    /// <c>min(HttpClient.Timeout, caller timeout)</c>. That is exactly what a 30 s
    /// client timeout did to <c>deploy site</c>'s 5-minute bulk deploy and to the
    /// five-minute <c>bundle</c> export/preview/import calls — each printed a fake
    /// <c>504 Request timed out</c> at 30 s while the server carried on working.
    /// <see cref="HttpClient.Timeout"/> is therefore
    /// <see cref="Timeout.InfiniteTimeSpan"/> and the per-call
    /// <see cref="CancellationTokenSource"/> in
    /// <see cref="SendCommandAsync"/>/<see cref="SendGetAsync"/>/<see cref="SendPostAsync"/>
    /// is the SINGLE overall deadline — it bounds connect too, since the token is
    /// passed into the send itself.
    /// </para>
    ///
    /// <para>
    /// The connect bound lives on <see cref="SocketsHttpHandler.ConnectTimeout"/>
    /// instead, which is connect-scoped and so cannot truncate a long-running
    /// request that has already reached the server. Overridable via the
    /// <c>SCADABRIDGE_HTTP_CONNECT_TIMEOUT_SECONDS</c> environment variable,
    /// consistent with how every other CLI setting is overridden (see
    /// <see cref="CliConfig"/>) — kept self-contained here (no
    /// <see cref="CliConfig"/>/command-file plumbing) since CLI commands are owned
    /// by a separate work package this phase.
    /// </para>
    /// </summary>
    public static readonly TimeSpan DefaultConnectTimeout = TimeSpan.FromSeconds(30);

    /// <summary>Test seam — the effective <see cref="HttpClient.Timeout"/> this instance was constructed with.</summary>
    internal TimeSpan EffectiveTimeout { get; }

    /// <summary>
    /// Resolves the effective connect timeout: the
    /// <c>SCADABRIDGE_HTTP_CONNECT_TIMEOUT_SECONDS</c> environment variable when set
    /// to a positive integer, otherwise <see cref="DefaultConnectTimeout"/>.
    /// </summary>
    /// <returns>The connect timeout to apply to the socket handler.</returns>
    internal static TimeSpan ResolveConnectTimeout()
    {
        var env = Environment.GetEnvironmentVariable("SCADABRIDGE_HTTP_CONNECT_TIMEOUT_SECONDS");
        if (!string.IsNullOrWhiteSpace(env)
            && int.TryParse(env, out var seconds)
            && seconds > 0)
        {
            return TimeSpan.FromSeconds(seconds);
        }

        return DefaultConnectTimeout;
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="ManagementHttpClient"/> class with
    /// an INFINITE <see cref="HttpClient.Timeout"/> (each call supplies its own
    /// deadline) over a <see cref="SocketsHttpHandler"/> whose
    /// <see cref="SocketsHttpHandler.ConnectTimeout"/> is
    /// <see cref="ResolveConnectTimeout"/>.
    /// </summary>
    /// <param name="baseUrl">The base URL for the management API.</param>
    /// <param name="username">The username for HTTP Basic authentication.</param>
    /// <param name="password">The password for HTTP Basic authentication.</param>
    public ManagementHttpClient(string baseUrl, string username, string password)
        : this(
            new HttpClient(new SocketsHttpHandler { ConnectTimeout = ResolveConnectTimeout() })
            {
                Timeout = Timeout.InfiniteTimeSpan
            },
            baseUrl, username, password)
    {
    }

    /// <summary>
    /// Test-only constructor that accepts a pre-built <see cref="HttpClient"/> (typically
    /// over a stub <see cref="HttpMessageHandler"/>) so the request/response handling can
    /// be exercised without a live server.
    /// </summary>
    /// <param name="httpClient">The HTTP client to use for requests.</param>
    /// <param name="baseUrl">The base URL for the management API.</param>
    /// <param name="username">The username for HTTP Basic authentication.</param>
    /// <param name="password">The password for HTTP Basic authentication.</param>
    internal ManagementHttpClient(HttpClient httpClient, string baseUrl, string username, string password)
    {
        _httpClient = httpClient;
        // Test seam (WP2.6e): exposes the constructed HttpClient's effective Timeout
        // without requiring reflection.
        EffectiveTimeout = httpClient.Timeout;
        _httpClient.BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/");
        var credentials = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}"));
        _httpClient.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Basic", credentials);
    }

    /// <summary>
    /// Sends a management command to the management API.
    /// </summary>
    /// <param name="commandName">The command name to execute.</param>
    /// <param name="payload">The command payload.</param>
    /// <param name="timeout">The request timeout.</param>
    /// <returns>A management response containing status and data.</returns>
    public async Task<ManagementResponse> SendCommandAsync(string commandName, object payload, TimeSpan timeout)
    {
        using var cts = new CancellationTokenSource(timeout);

        var body = JsonSerializer.Serialize(new { command = commandName, payload },
            new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });

        var content = new StringContent(body, Encoding.UTF8, "application/json");

        HttpResponseMessage httpResponse;
        try
        {
            httpResponse = await _httpClient.PostAsync("management", content, cts.Token);
        }
        catch (TaskCanceledException)
        {
            return new ManagementResponse(504, null, "Request timed out.", "TIMEOUT");
        }
        catch (HttpRequestException ex)
        {
            return new ManagementResponse(0, null, $"Connection failed: {ex.Message}", "CONNECTION_FAILED");
        }

        var responseBody = await httpResponse.Content.ReadAsStringAsync(cts.Token);

        if (httpResponse.IsSuccessStatusCode)
        {
            return new ManagementResponse((int)httpResponse.StatusCode, responseBody, null, null);
        }

        // Parse error response
        string? error = null;
        string? code = null;
        try
        {
            using var doc = JsonDocument.Parse(responseBody);
            error = doc.RootElement.TryGetProperty("error", out var e) ? e.GetString() : responseBody;
            code = doc.RootElement.TryGetProperty("code", out var c) ? c.GetString() : null;
        }
        catch
        {
            error = responseBody;
        }

        return new ManagementResponse((int)httpResponse.StatusCode, null, error, code);
    }

    /// <summary>
    /// Issues a plain HTTP <c>GET</c> against a REST endpoint (e.g. the audit
    /// <c>/api/audit/query</c> endpoint) and returns the
    /// response body. Unlike <see cref="SendCommandAsync"/>, this does not wrap the call
    /// in the <c>POST /management</c> command envelope — the audit endpoints are plain
    /// REST resources. Authentication (HTTP Basic) and the base address are shared.
    /// </summary>
    /// <param name="relativePath">Path relative to the base URL, with query string.</param>
    /// <param name="timeout">The request timeout.</param>
    /// <returns>A management response containing status and data.</returns>
    public async Task<ManagementResponse> SendGetAsync(string relativePath, TimeSpan timeout)
    {
        using var cts = new CancellationTokenSource(timeout);

        HttpResponseMessage httpResponse;
        try
        {
            httpResponse = await _httpClient.GetAsync(relativePath, cts.Token);
        }
        catch (TaskCanceledException)
        {
            return new ManagementResponse(504, null, "Request timed out.", "TIMEOUT");
        }
        catch (HttpRequestException ex)
        {
            return new ManagementResponse(0, null, $"Connection failed: {ex.Message}", "CONNECTION_FAILED");
        }

        var responseBody = await httpResponse.Content.ReadAsStringAsync(cts.Token);

        if (httpResponse.IsSuccessStatusCode)
        {
            return new ManagementResponse((int)httpResponse.StatusCode, responseBody, null, null);
        }

        string? error = null;
        string? code = null;
        try
        {
            using var doc = JsonDocument.Parse(responseBody);
            error = doc.RootElement.TryGetProperty("error", out var e) ? e.GetString() : responseBody;
            code = doc.RootElement.TryGetProperty("code", out var c) ? c.GetString() : null;
        }
        catch
        {
            error = responseBody;
        }

        return new ManagementResponse((int)httpResponse.StatusCode, null, error, code);
    }

    /// <summary>
    /// Issues a plain HTTP <c>POST</c> against a REST endpoint (e.g. the audit
    /// maintenance endpoints) with a JSON body and returns the response. Unlike
    /// <see cref="SendCommandAsync"/>, this does not wrap the call in the
    /// <c>POST /management</c> command envelope — these are plain REST resources.
    /// Authentication (HTTP Basic) and the base address are shared.
    /// </summary>
    /// <param name="relativePath">Path relative to the base URL.</param>
    /// <param name="body">The JSON body to send, or <c>null</c> for an empty body.</param>
    /// <param name="timeout">The request timeout.</param>
    /// <returns>A management response containing status and data.</returns>
    public async Task<ManagementResponse> SendPostAsync(string relativePath, string? body, TimeSpan timeout)
    {
        using var cts = new CancellationTokenSource(timeout);

        var content = new StringContent(body ?? "{}", Encoding.UTF8, "application/json");

        HttpResponseMessage httpResponse;
        try
        {
            httpResponse = await _httpClient.PostAsync(relativePath, content, cts.Token);
        }
        catch (TaskCanceledException)
        {
            return new ManagementResponse(504, null, "Request timed out.", "TIMEOUT");
        }
        catch (HttpRequestException ex)
        {
            return new ManagementResponse(0, null, $"Connection failed: {ex.Message}", "CONNECTION_FAILED");
        }

        var responseBody = await httpResponse.Content.ReadAsStringAsync(cts.Token);

        if (httpResponse.IsSuccessStatusCode)
        {
            return new ManagementResponse((int)httpResponse.StatusCode, responseBody, null, null);
        }

        string? error = null;
        string? code = null;
        try
        {
            using var doc = JsonDocument.Parse(responseBody);
            error = doc.RootElement.TryGetProperty("error", out var e) ? e.GetString() : responseBody;
            code = doc.RootElement.TryGetProperty("code", out var c) ? c.GetString() : null;
        }
        catch
        {
            error = responseBody;
        }

        return new ManagementResponse((int)httpResponse.StatusCode, null, error, code);
    }

    /// <summary>
    /// Issues a plain HTTP <c>GET</c> and returns the raw <see cref="HttpResponseMessage"/>
    /// so the caller can stream the response body without buffering it in memory — used
    /// by <c>audit export</c>, where the response can be many megabytes. The caller owns
    /// disposing the returned message. The <see cref="HttpCompletionOption.ResponseHeadersRead"/>
    /// option ensures the body is not pre-buffered.
    /// </summary>
    /// <param name="relativePath">Path relative to the base URL, with query string.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>The raw HTTP response message for streaming.</returns>
    public async Task<HttpResponseMessage> SendGetStreamAsync(string relativePath, CancellationToken cancellationToken)
        => await _httpClient.GetAsync(relativePath, HttpCompletionOption.ResponseHeadersRead, cancellationToken);

    /// <summary>
    /// Disposes the underlying HTTP client.
    /// </summary>
    public void Dispose() => _httpClient.Dispose();
}

public record ManagementResponse(int StatusCode, string? JsonData, string? Error, string? ErrorCode);
