using System.ComponentModel.DataAnnotations;
using System.Text;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Driver.Mqtt;
///
/// MQTT / Sparkplug B driver configuration. Bound from DriverConfig JSON at
/// driver-host registration time. Models the settings documented in
/// docs/plans/2026-07-15-mqtt-sparkplug-driver-design.md §5.1.
///
///
/// A record (not a plain class) so a future secret-resolution seam can produce a
/// credential-resolved copy with a with expression — mirrors
/// OpcUaClientDriverOptions. selects the ingest shape;
/// and are nullable sub-objects — only the one
/// matching is populated.
///
public sealed record MqttDriverOptions
{
/// Broker hostname or IP address.
public string Host { get; init; } = "localhost";
/// Broker TCP port.
[Range(1, 65535)]
public int Port { get; init; } = 8883;
///
/// MQTT client identifier sent at CONNECT. Leave unset to let the driver generate one
/// (a stable per-instance id is recommended so broker-side ACLs / session state persist
/// across reconnects).
///
public string? ClientId { get; init; }
///
/// When true, connect over TLS. Default true — this driver never ships an
/// anonymous/plaintext-by-default posture; a deployment must opt into false for an
/// on-prem/dev broker with no TLS listener.
///
public bool UseTls { get; init; } = true;
///
/// When true, accept any self-signed / untrusted broker certificate. Dev/on-prem
/// escape hatch only — mirrors the ServerHistorian TLS knobs. Must stay
/// false in production so MITM attacks against the broker connection fail closed.
///
public bool AllowUntrustedServerCertificate { get; init; } = false;
///
/// PEM CA file pinning the broker's TLS chain. null/empty uses the OS trust
/// store.
///
public string? CaCertificatePath { get; init; }
/// Username for broker authentication. null connects without a username.
public string? Username { get; init; }
///
/// Password for broker authentication. Blank in committed JSON — supply via env, never
/// commit or log.
///
public string? Password { get; init; } = "";
/// MQTT protocol version to negotiate at CONNECT.
public MqttProtocolVersion ProtocolVersion { get; init; } = MqttProtocolVersion.V500;
/// Whether to request a clean session (v3.1.1) / clean start (v5.0) at CONNECT.
public bool CleanSession { get; init; } = true;
/// Keep-alive interval sent at CONNECT.
[Range(1, int.MaxValue)]
public int KeepAliveSeconds { get; init; } = 30;
/// Bounded connect deadline (see design §8) — the driver never hangs past this.
[Range(1, int.MaxValue)]
public int ConnectTimeoutSeconds { get; init; } = 15;
/// Initial reconnect backoff after a connection drop.
[Range(1, int.MaxValue)]
public int ReconnectMinBackoffSeconds { get; init; } = 1;
/// Cap on the exponential reconnect backoff.
[Range(1, int.MaxValue)]
public int ReconnectMaxBackoffSeconds { get; init; } = 30;
///
/// Ceiling on an inbound message body, in bytes. A larger message is refused before any
/// decode or parse and degrades its own tags to BadDecodingError. Default 1 MiB.
///
///
/// Decode and parse both run synchronously on MQTTnet's shared dispatcher thread, so their
/// cost is paid by every subscription, not just the offending topic — without a bound,
/// one publisher shipping a multi-megabyte body stalls the whole driver's delivery. "Unbounded"
/// is deliberately not offered; a non-positive value falls back to the driver's own 1 MiB
/// default. The literal is duplicated from MqttSubscriptionManager.DefaultMaxPayloadBytes
/// because this .Contracts assembly is referenced by the driver assembly and
/// cannot reference it back; the manager's constructor is the single enforcement point.
///
[Range(1, int.MaxValue)]
public int MaxPayloadBytes { get; init; } = 1024 * 1024;
/// Selects the ingest shape — Plain topics or Sparkplug B.
public MqttMode Mode { get; init; } = MqttMode.Plain;
///
/// The cluster's authored raw MQTT tags, as delivered by the deploy artifact: each carries the
/// tag's RawPath (its v3 identity and driver wire reference) plus its TagConfig
/// blob. maps each through at
/// initialize and re-registers the set wholesale on every reinitialize, so a redeploy
/// that drops a tag stops feeding it.
///
///
/// This is also the only source of the driver's discoverable node set — plain MQTT has
/// no browsable address space, and a tag that is not authored here is never materialized no
/// matter how much traffic its topic carries. Mirrors ModbusDriverOptions.RawTags /
/// FocasDriverOptions.RawTags.
///
public IReadOnlyList RawTags { get; init; } = [];
///
/// Sparkplug-only settings. Populated when is
/// ; null in Plain mode.
///
public MqttSparkplugOptions? Sparkplug { get; init; }
///
/// Plain-mode-only settings. Populated when is
/// ; null in Sparkplug B mode.
///
public MqttPlainOptions? Plain { get; init; }
///
/// Record-generated ToString() member printer, overridden so
/// never renders in plaintext — this DTO routinely lands in logs / exception messages /
/// debugger watches, and the plan's cross-cutting rule is explicit: never commit or log
/// creds. A set password prints as ***; unset (null/empty) renders
/// distinguishably so the redaction can't be mistaken for a real secret.
///
private bool PrintMembers(StringBuilder builder)
{
var passwordDisplay = Password switch
{
null => "",
"" => "",
_ => "***",
};
builder.Append("Host = ").Append(Host);
builder.Append(", Port = ").Append(Port);
builder.Append(", ClientId = ").Append(ClientId);
builder.Append(", UseTls = ").Append(UseTls);
builder.Append(", AllowUntrustedServerCertificate = ").Append(AllowUntrustedServerCertificate);
builder.Append(", CaCertificatePath = ").Append(CaCertificatePath);
builder.Append(", Username = ").Append(Username);
builder.Append(", Password = ").Append(passwordDisplay);
builder.Append(", ProtocolVersion = ").Append(ProtocolVersion);
builder.Append(", CleanSession = ").Append(CleanSession);
builder.Append(", KeepAliveSeconds = ").Append(KeepAliveSeconds);
builder.Append(", ConnectTimeoutSeconds = ").Append(ConnectTimeoutSeconds);
builder.Append(", ReconnectMinBackoffSeconds = ").Append(ReconnectMinBackoffSeconds);
builder.Append(", ReconnectMaxBackoffSeconds = ").Append(ReconnectMaxBackoffSeconds);
builder.Append(", MaxPayloadBytes = ").Append(MaxPayloadBytes);
builder.Append(", Mode = ").Append(Mode);
builder.Append(", Sparkplug = ").Append(Sparkplug);
builder.Append(", Plain = ").Append(Plain);
// Count only: a deployment routinely authors thousands of raw tags and each carries a full
// TagConfig blob, so printing the list would turn any log of this DTO into a config dump.
builder.Append(", RawTags = ").Append(RawTags.Count).Append(" tag(s)");
return true;
}
}
///
/// Sparkplug B settings for an instance in
/// mode. See
/// docs/plans/2026-07-15-mqtt-sparkplug-driver-design.md §5.1.
///
public sealed record MqttSparkplugOptions
{
/// Sparkplug group id — the driver subscribes spBv1.0/{GroupId}/#.
public string GroupId { get; init; } = "";
///
/// Sparkplug Host Application identity — published as spBv1.0/STATE/{HostId}
/// (Sparkplug v3.0).
///
public string HostId { get; init; } = "";
///
/// When true, this driver instance acts as the Sparkplug Primary Host
/// Application. At most one primary host may exist per host-id per broker.
///
///
/// Implemented by SparkplugPrimaryHost (Gitea #511) on a dedicated broker connection
/// whose lifetime is the claim, and gated on the redundancy Primary role so the two nodes
/// of a pair never both claim. Requires ; set without one, the driver warns
/// and declines rather than publishing to a malformed topic.
///
public bool ActAsPrimaryHost { get; init; } = false;
///
/// When true, a detected sequence-number gap triggers a Sparkplug rebirth
/// request (NCMD) rather than silently continuing with stale metric state.
///
public bool RequestRebirthOnGap { get; init; } = true;
///
/// How long, in seconds, browse/discovery collects NBIRTH/DBIRTH traffic before
/// considering the observed metric set stable.
///
[Range(1, int.MaxValue)]
public int BirthObservationWindowSeconds { get; init; } = 15;
}
///
/// Plain-MQTT-mode settings for an instance in
/// mode. See
/// docs/plans/2026-07-15-mqtt-sparkplug-driver-design.md §5.1.
///
public sealed record MqttPlainOptions
{
///
/// Optional prefix prepended when the driver needs to compose a topic (e.g. discovery
/// scoping); per-tag topics are authored explicitly and are unaffected.
///
public string TopicPrefix { get; init; } = "";
/// Default QoS used when a tag's own qos is unset.
[Range(0, 2)]
public int DefaultQos { get; init; } = 1;
}