using Microsoft.Extensions.Configuration;
using ZB.MOM.WW.Configuration;
using ZB.MOM.WW.OtOpcUa.Runtime.Historian;
namespace ZB.MOM.WW.OtOpcUa.Host.Configuration;
///
/// Fail-fast startup validator for (archreview 06/S-11),
/// built on the shared ZB.MOM.WW.Configuration .
/// Wired via AddValidatedOptions so a provably-crashing historian config fails host startup
/// with a named, aggregated OptionsValidationException instead of the raw
/// the gateway client factory's new Uri(...) would
/// otherwise throw at first resolution (a crash-loop under a service/docker restart policy).
///
///
///
/// Consumer-gated, not section-gated. The load-bearing question is not "is
/// ServerHistorian enabled?" but "is its gateway connection consumed by anything
/// enabled?". The alarm-history sink (AlarmHistorian:Enabled=true) sources its
/// endpoint/key/TLS from the ServerHistorian section independently of
/// , so it must also gate this check — otherwise the
/// AlarmHistorian:Enabled=true / ServerHistorian:Enabled=false corner crashes with
/// zero warning. Continuous historization is already co-gated on
/// in the Host, so Enabled covers it.
///
///
/// Fail tier = provably-crashing configs only. Three settings qualify, and they all fail
/// the same way — HistorianGatewayClientOptions.Validate() throws inside the client
/// factory before any call is attempted, so the host dies at startup:
/// an empty / non-absolute / non-http(s) Endpoint; an empty ApiKey ("The gateway
/// API key must not be empty"); and a UseTls that disagrees with the endpoint scheme
/// ("UseTls requires an https gateway endpoint" / "An https gateway endpoint requires UseTls").
/// A non-positive MaxTieClusterOverfetch genuinely degrades rather than crashes (the node
/// manager surfaces a Bad read), so it stays an operator warning in
/// . The Endpoint value is not a secret and
/// is echoed to make each error actionable; the ApiKey is never surfaced.
///
///
/// The ApiKey and UseTls checks were added after two consecutive live-rig
/// crash-loops (LocalDb Phase 2 gate). Both had been classified as degrading, on the
/// assumption that a bad client configuration would connect and be rejected by the gateway — but
/// the client validates its own options at construction, so the process dies during Akka startup
/// instead, which is precisely the failure this validator exists to convert into a named,
/// aggregated error. The lesson generalises: "degrades" is only true of settings the client
/// does not itself validate.
///
///
public sealed class ServerHistorianOptionsValidator : OptionsValidatorBase
{
private readonly IConfiguration _configuration;
/// Creates the validator over the app configuration (read to resolve the consumer gate).
/// The app configuration; used to read AlarmHistorian:Enabled.
public ServerHistorianOptionsValidator(IConfiguration configuration) =>
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
///
protected override void Validate(ValidationBuilder builder, ServerHistorianOptions options)
{
var alarmHistorianEnabled = _configuration.GetValue("AlarmHistorian:Enabled");
var consumed = options.Enabled || alarmHistorianEnabled;
// A section nobody consumes may legitimately be empty (the docker-dev default) — no checks.
if (!consumed) return;
var endpointValid = Uri.TryCreate(options.Endpoint, UriKind.Absolute, out var uri)
&& (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps);
// Name the DRIVING section(s) so the operator knows why an empty/malformed endpoint fails —
// especially in the alarm-only corner where ServerHistorian itself is disabled.
var reason = options.Enabled
? "required because ServerHistorian:Enabled=true"
: "required because AlarmHistorian:Enabled=true sources its gateway connection (endpoint/key/TLS) from the ServerHistorian section";
builder.RequireThat(
endpointValid,
$"ServerHistorian:Endpoint is empty or not an absolute http(s) URI ('{options.Endpoint}') — {reason}.");
// Deliberately not echoed: unlike the endpoint, the key is a secret.
builder.RequireThat(
!string.IsNullOrWhiteSpace(options.ApiKey),
$"ServerHistorian:ApiKey is empty — {reason}. The gateway client rejects a keyless "
+ "configuration at construction, so the host would crash on startup rather than degrade. "
+ "Supply it via the environment variable ServerHistorian__ApiKey.");
// The flag and the scheme must agree in BOTH directions — the client throws either way
// ("UseTls requires an https gateway endpoint" / "An https gateway endpoint requires UseTls").
// Only meaningful once the endpoint parsed; a malformed one already failed above.
if (endpointValid)
{
var https = uri!.Scheme == Uri.UriSchemeHttps;
builder.RequireThat(
options.UseTls == https,
$"ServerHistorian:UseTls is {options.UseTls.ToString().ToLowerInvariant()} but the "
+ $"endpoint is '{options.Endpoint}' — {reason}. The flag must match the scheme "
+ "(https ⇒ UseTls=true, http ⇒ UseTls=false); the gateway client rejects a mismatch "
+ "at construction, crashing the host on startup.");
}
}
}