using System.Security.Cryptography;
using System.Text;
using Microsoft.Data.Sqlite;
namespace ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian;
///
/// DDL for the alarm store-and-forward buffer: one row per alarm event awaiting delivery to
/// the historian gateway.
///
///
///
/// Replaces the standalone alarm-historian.db the sink used to own outright. Living
/// in the consolidated LocalDb file is what lets the buffer replicate to the redundant pair
/// peer, so a node that dies with undelivered alarm history no longer takes it to the grave.
///
///
/// Deliberately depends on nothing but so it can be applied
/// to any connection — the host's LocalDbSetup.OnReady in production, and a bare
/// connection in a test — without dragging the DI graph along. Mirrors
/// DeploymentCacheSchema.
///
///
/// Why is TEXT and app-minted. The legacy queue keyed on
/// RowId INTEGER PRIMARY KEY AUTOINCREMENT. Convergence is last-writer-wins over the
/// primary key, so two nodes independently allocating rowid 7 for different alarms would
/// silently overwrite one another. The sink mints the id from a hash of the payload, which
/// additionally makes the same event converge to one row when both nodes of a pair enqueue
/// it — as they legitimately do in the window before the first redundancy snapshot arrives.
///
///
public static class AlarmSfSchema
{
/// Table holding queued alarm events awaiting delivery.
public const string EventsTable = "alarm_sf_events";
/// The single primary-key column.
public const string IdColumn = "id";
///
/// Derives a row's primary key from its serialized payload.
///
///
///
/// Deterministic rather than a fresh GUID, so that the same event arriving twice
/// converges to one row under last-writer-wins instead of duplicating. That happens for
/// real in two places: HistorianAdapterActor default-writes while its redundancy
/// role is unknown, so both nodes of a pair accept the same fanned transition in the
/// window before the first snapshot; and both nodes independently migrate their own
/// legacy queue file, which for a warm pair holds overlapping history.
///
///
/// Two genuinely distinct events cannot collide. AlarmHistorianEvent carries a
/// full-precision timestamp alongside the alarm id, transition kind, message and user,
/// so an equal hash means an equal event.
///
///
/// The serialized AlarmHistorianEvent.
/// The row's primary key.
public static string DeriveId(string payloadJson)
{
ArgumentNullException.ThrowIfNull(payloadJson);
return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(payloadJson)));
}
///
/// Creates the buffer table if it does not already exist. Idempotent.
///
///
/// An already-open connection. ILocalDb.CreateConnection() hands out open,
/// pragma-configured connections — do not call Open() on one.
///
public static void Apply(SqliteConnection connection)
{
ArgumentNullException.ThrowIfNull(connection);
using var cmd = connection.CreateCommand();
// Columns map 1:1 onto the legacy Queue table so the one-time migrator is a straight copy
// and the drain keeps its existing semantics: dead_lettered is the same 0/1 flag, and an
// acknowledged row is DELETEd rather than marked. Deleting keeps the table bounded without
// a second sweeper, and the replication engine carries the delete as a tombstone, so the
// peer drops its copy too.
//
// The drain index orders by enqueued_at_utc because a hashed TEXT id carries no insertion
// order the way the legacy AUTOINCREMENT RowId did. Timestamps are round-trip ("O") format,
// so lexicographic ordering is chronological; id breaks ties so the order is total and the
// index covers the drain's read.
cmd.CommandText = """
CREATE TABLE IF NOT EXISTS alarm_sf_events (
id TEXT NOT NULL PRIMARY KEY,
alarm_id TEXT NOT NULL,
enqueued_at_utc TEXT NOT NULL,
payload_json TEXT NOT NULL,
attempt_count INTEGER NOT NULL DEFAULT 0,
last_attempt_utc TEXT NULL,
last_error TEXT NULL,
dead_lettered INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS ix_alarm_sf_events_drain
ON alarm_sf_events (dead_lettered, enqueued_at_utc, id);
""";
cmd.ExecuteNonQuery();
}
}