using System.Globalization;
using System.Text.Json;
namespace ZB.MOM.WW.ScadaBridge.CLI.Commands;
///
/// Helpers for the audit verify-chain subcommand (ScadaBridge#41). The command sends
/// the VerifyAuditChain management command for one UTC calendar month and reports
/// whether that month's AuditLog hash chain is intact, or the first break (chain partition,
/// ChainSeq, EventId, kind).
///
///
/// Exit codes: 0 intact (or nothing chained yet), when
/// the chain is broken, 1 on a request error, 2 on an authorization failure.
///
public static class AuditVerifyChainHelpers
{
/// Process exit code when verification found a break (tamper evidence).
public const int BrokenExitCode = 3;
///
/// Client deadline. The server bounds the call with the long-running management Ask
/// (5 min); the client waits a little longer so the server's answer always arrives.
///
public static readonly TimeSpan RequestTimeout = TimeSpan.FromMinutes(6);
///
/// Returns true if is a well-formed YYYY-MM value
/// with a real month (01-12). A malformed month (e.g. 2026-13) is rejected.
///
/// The month string to validate in YYYY-MM format.
/// true if the string is a well-formed YYYY-MM value with a real month; otherwise false.
public static bool IsValidMonth(string? month)
=> !string.IsNullOrWhiteSpace(month)
&& DateTime.TryParseExact(month, "yyyy-MM", CultureInfo.InvariantCulture,
DateTimeStyles.None, out _);
///
/// Renders a VerifyAuditChain result and maps it to the process exit code.
/// json prints the server body verbatim; table prints a short report.
///
/// The success body (an AuditChainVerificationResult, camelCase).
/// Resolved output format.
/// Writer for the report.
/// 0 when intact / not chained, when broken.
public static int Render(string json, string format, TextWriter output)
{
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
var status = root.TryGetProperty("status", out var s) ? s.GetString() ?? "" : "";
var broken = string.Equals(status, "Broken", StringComparison.OrdinalIgnoreCase);
if (!string.Equals(format, "table", StringComparison.OrdinalIgnoreCase))
{
output.WriteLine(json);
output.Flush();
return broken ? BrokenExitCode : 0;
}
output.WriteLine($"Partition : {Str(root, "partition")}");
output.WriteLine($"Status : {status}");
output.WriteLine($"Rows verified : {Num(root, "rowsVerified")}");
output.WriteLine($"Tombstones applied: {Num(root, "tombstonesApplied")}");
output.WriteLine($"Purged through : {Num(root, "purgedThroughSeq", "-")}");
output.WriteLine($"Head sequence : {Num(root, "headSeq", "-")}");
output.WriteLine($"Unchained rows : {Num(root, "unchainedRows")}");
if (root.TryGetProperty("firstBreak", out var brk) && brk.ValueKind == JsonValueKind.Object)
{
output.WriteLine("First break:");
output.WriteLine($" partition : {Str(brk, "partition")}");
output.WriteLine($" chainSeq : {Num(brk, "chainSeq")}");
output.WriteLine($" eventId : {Str(brk, "eventId", "-")}");
output.WriteLine($" kind : {Str(brk, "kind")}");
output.WriteLine($" detail : {Str(brk, "detail")}");
}
output.Flush();
return broken ? BrokenExitCode : 0;
}
private static string Str(JsonElement e, string name, string missing = "")
=> e.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String ? v.GetString() ?? missing : missing;
private static string Num(JsonElement e, string name, string missing = "0")
=> e.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.Number ? v.GetInt64().ToString(CultureInfo.InvariantCulture) : missing;
}