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

namespace ZB.MOM.WW.ScadaBridge.CLI.Commands;

/// <summary>
/// Filter + destination arguments for an <c>audit export</c> invocation. Mirrors the
/// <c>GET /api/audit/export</c> parameters.
/// <see cref="Channel"/>/<see cref="Kind"/>/<see cref="Status"/>/<see cref="Site"/>
/// are multi-valued — each supplied value becomes a repeated query-string param so
/// the server's multi-value <c>IN (…)</c> filter sees the full set, exactly like
/// the <c>audit query</c> subcommand.
/// </summary>
public sealed class AuditExportArgs
{
    /// <summary>
    /// Start timestamp for the export time window.
    /// </summary>
    public string Since { get; set; } = string.Empty;
    /// <summary>
    /// End timestamp for the export time window.
    /// </summary>
    public string Until { get; set; } = string.Empty;
    /// <summary>
    /// Export format (e.g., 'json', 'csv', 'parquet').
    /// </summary>
    public string Format { get; set; } = string.Empty;
    /// <summary>
    /// Output file path for the exported audit log.
    /// </summary>
    public string Output { get; set; } = string.Empty;
    /// <summary>
    /// Channel filter values (repeated query parameter).
    /// </summary>
    public string[] Channel { get; set; } = Array.Empty<string>();
    /// <summary>
    /// Kind filter values (repeated query parameter).
    /// </summary>
    public string[] Kind { get; set; } = Array.Empty<string>();
    /// <summary>
    /// Status filter values (repeated query parameter).
    /// </summary>
    public string[] Status { get; set; } = Array.Empty<string>();
    /// <summary>
    /// Site identifier filter values (repeated query parameter).
    /// </summary>
    public string[] Site { get; set; } = Array.Empty<string>();
    /// <summary>
    /// Optional target system filter.
    /// </summary>
    public string? Target { get; set; }
    /// <summary>
    /// Optional actor/user filter.
    /// </summary>
    public string? Actor { get; set; }
}

/// <summary>
/// Helpers for the <c>audit export</c> subcommand: builds the export query string and
/// streams the HTTP response body straight to the destination file without buffering
/// the (potentially multi-megabyte) export in memory.
/// </summary>
public static class AuditExportHelpers
{
    /// <summary>
    /// Builds the <c>?...</c> query string for <c>GET /api/audit/export</c>: the required
    /// time window + format, plus optional filters. Time-specs are resolved via
    /// <see cref="AuditQueryHelpers.ResolveTimeSpec"/>. The multi-valued
    /// <c>--channel</c>/<c>--kind</c>/<c>--status</c>/<c>--site</c> filters each emit ONE
    /// repeated query-string key per value (e.g. <c>channel=A&amp;channel=B</c>) so the
    /// server's multi-value <c>IN (…)</c> filter receives the full set — mirroring
    /// <see cref="AuditQueryHelpers.BuildQueryString"/>.
    /// </summary>
    /// <param name="args">The export arguments containing filters and format.</param>
    /// <param name="now">The current time for resolving relative time specifications.</param>
    /// <returns>The full query string (including the leading <c>?</c>) for the export endpoint.</returns>
    public static string BuildQueryString(AuditExportArgs args, DateTimeOffset now)
    {
        var parts = new List<string>();

        void Add(string key, string? value)
        {
            if (!string.IsNullOrWhiteSpace(value))
                parts.Add($"{key}={Uri.EscapeDataString(value)}");
        }

        void AddEach(string key, IReadOnlyList<string> values)
        {
            foreach (var value in values)
            {
                Add(key, value);
            }
        }

        Add("fromUtc", AuditQueryHelpers.ResolveTimeSpec(args.Since, now).ToString("o", CultureInfo.InvariantCulture));
        Add("toUtc", AuditQueryHelpers.ResolveTimeSpec(args.Until, now).ToString("o", CultureInfo.InvariantCulture));
        Add("format", args.Format);
        AddEach("channel", args.Channel);
        AddEach("kind", args.Kind);
        AddEach("status", args.Status);
        AddEach("sourceSiteId", args.Site);
        Add("target", args.Target);
        Add("actor", args.Actor);

        return "?" + string.Join("&", parts);
    }

    /// <summary>
    /// Executes the export: GETs <c>/api/audit/export</c> and copies the response body
    /// stream directly to <see cref="AuditExportArgs.Output"/>. The body is never fully
    /// buffered — <see cref="Stream.CopyToAsync(Stream)"/> streams in fixed-size chunks.
    /// A <c>501 Not Implemented</c> (parquet not yet supported server-side) prints the
    /// server message and returns a non-zero exit code.
    /// </summary>
    /// <param name="client">The management HTTP client for API communication.</param>
    /// <param name="args">The export arguments containing filters and output file path.</param>
    /// <param name="output">Text writer for command output messages.</param>
    /// <param name="now">The current time for resolving relative time specifications.</param>
    /// <returns>0 on success, 1 on general error, or 2 on authorization failure.</returns>
    public static async Task<int> RunExportAsync(
        ManagementHttpClient client, AuditExportArgs args, TextWriter output, DateTimeOffset now)
    {
        var qs = BuildQueryString(args, now);

        HttpResponseMessage response;
        try
        {
            response = await client.SendGetStreamAsync("api/audit/export" + qs, CancellationToken.None);
        }
        catch (HttpRequestException ex)
        {
            OutputFormatter.WriteError($"Connection failed: {ex.Message}", "CONNECTION_FAILED");
            return 1;
        }

        using (response)
        {
            if (response.StatusCode == HttpStatusCode.NotImplemented)
            {
                var message = await response.Content.ReadAsStringAsync();
                OutputFormatter.WriteError(
                    string.IsNullOrWhiteSpace(message)
                        ? "Export format not implemented by the server."
                        : message,
                    "NOT_IMPLEMENTED");
                return 1;
            }

            if (!response.IsSuccessStatusCode)
            {
                var message = await response.Content.ReadAsStringAsync();
                // Honour the documented "authorization failure → exit 2"
                // contract on the REST audit surface as well. HTTP 403 is the
                // primary signal; the server may also surface UNAUTHORIZED /
                // FORBIDDEN via the JSON error envelope on a non-403 status.
                var errorCode = TryExtractErrorCode(message);
                var isAuthFailure = (int)response.StatusCode == 403
                    || string.Equals(errorCode, "FORBIDDEN", StringComparison.OrdinalIgnoreCase)
                    || string.Equals(errorCode, "UNAUTHORIZED", StringComparison.OrdinalIgnoreCase);
                OutputFormatter.WriteError(
                    string.IsNullOrWhiteSpace(message) ? $"Export failed (HTTP {(int)response.StatusCode})." : message,
                    errorCode ?? "ERROR");
                return isAuthFailure ? 2 : 1;
            }

            await using var source = await response.Content.ReadAsStreamAsync();
            await using var destination = new FileStream(
                args.Output, FileMode.Create, FileAccess.Write, FileShare.None,
                bufferSize: 81920, useAsync: true);
            await source.CopyToAsync(destination);
        }

        output.WriteLine($"Exported audit log to {args.Output}");
        return 0;
    }

    /// <summary>
    /// Best-effort parse of the server's JSON error envelope (<c>{ "error": ..., "code": ... }</c>)
    /// to extract the <c>code</c> field. Returns null if the body is empty, not valid JSON, or
    /// has no <c>code</c> property — callers fall back to "ERROR" in that case.
    /// </summary>
    /// <param name="body">The HTTP response body string to parse for an error code.</param>
    /// <returns>The <c>code</c> string from the JSON error envelope, or null if absent or unparseable.</returns>
    internal static string? TryExtractErrorCode(string body)
    {
        if (string.IsNullOrWhiteSpace(body))
            return null;

        try
        {
            using var doc = JsonDocument.Parse(body);
            if (doc.RootElement.ValueKind == JsonValueKind.Object
                && doc.RootElement.TryGetProperty("code", out var codeProp)
                && codeProp.ValueKind == JsonValueKind.String)
            {
                return codeProp.GetString();
            }
        }
        catch (JsonException)
        {
            // Body is not a JSON envelope (e.g. an HTML proxy error page); no code to extract.
        }

        return null;
    }
}
