namespace ZB.MOM.WW.ScadaBridge.SiteEventLogging;
public class SiteEventLogOptions
{
/// Number of days to retain site event log entries before purge; default 30.
public int RetentionDays { get; set; } = 30;
///
/// Maximum size in megabytes of site_events AND ITS FOUR INDEXES before the oldest
/// entries are trimmed; default 1024 MB.
///
///
///
/// This is not a cap on the database file (register row 81, decision record
/// docs/plans/2026-08-17-site-event-log-cap-semantics-decision.md). site_events
/// shares one SQLite file at LocalDb:Path with nine other tables plus LocalDb's
/// __localdb_oplog / __localdb_row_version bookkeeping. Measuring the whole file
/// made the trim unwinnable on a replication-enabled node — each batch of deletes generated
/// an equal number of oplog rows and tombstones in the same file, so the measured size never
/// fell and the trim stopped 2.5x over cap. Worse, it made the event log's budget a function
/// of the replication peer's health, in the direction where a replication fault destroys
/// operational history.
///
///
/// Consequence for sizing a site's data volume: this value alone does not bound it. LocalDb
/// replication bookkeeping is separate overhead on the same disk, bounded by
/// LocalDb:Replication:MaxOplogRows and TombstoneRetention. Budget the sum.
///
///
/// The footprint is MODELLED from sampled row width rather than measured exactly: SQLite's
/// dbstat virtual table would give an exact per-table page count but is not compiled
/// into the bundled native library. Measured accuracy is −0.8 % to +12.8 %, deliberately
/// biased high. See EventLogPurgeService.EstimateSiteEventsSize.
///
///
public int MaxStorageMb { get; set; } = 1024;
///
/// MIGRATION-ONLY now that no longer reads
/// this: site_events lives in the consolidated site database at
/// LocalDb:Path. The only remaining consumer is
/// SiteLocalDbLegacyMigrator, which uses it to locate a pre-consolidation
/// site_events.db and copy it in once. Safe to delete from a config once that
/// node's migration has run.
///
public string DatabasePath { get; set; } = "site_events.db";
/// Maximum number of rows returned per paginated query; default 500.
public int QueryPageSize { get; set; } = 500;
///
/// Hard upper bound on a caller-supplied PageSize. A
/// misbehaving or hostile central client that requests int.MaxValue would
/// otherwise force the query to materialise the entire log into a single list while
/// holding the shared write lock. Silent clamp; default 500 matches
/// .
///
public int MaxQueryPageSize { get; set; } = 500;
/// Interval between purge runs; default 24 hours.
public TimeSpan PurgeInterval { get; set; } = TimeSpan.FromHours(24);
///
/// Ceiling on how many site_events rows ONE purge tick may delete, across the
/// retention purge and the storage-cap trim combined. Default 250 000; 0 means
/// unlimited (the pre-pacing behaviour, kept as an escape hatch).
///
///
///
/// Why a ceiling exists at all. site_events is CDC-captured on a
/// replication-enabled node and there is deliberately no purge-exemption path, so a purge
/// queues one oplog row per deleted event. Measured worst case (register row 34,
/// docs/plans/2026-08-15-site-events-purge-burst-measurement.md): a 1 GB event log
/// holds ~2.73 million rows at 393 bytes/row, and expiring all of them in one tick queued
/// 2 731 947 oplog rows — 2.7x over LocalDb's MaxOplogRows default of
/// 1 000 000. Crossing that ceiling makes LocalDb's maintenance tick prune the oplog
/// and set needs_snapshot, which costs a FULL snapshot resync of all ten replicated
/// tables. The ceiling keeps a single tick's burst safely inside that budget (250 000 is
/// a 4x margin) while keeps retention converging.
///
///
/// The remainder is not dropped: a tick that hits the ceiling schedules the next tick after
/// instead of , so a backlog
/// drains over minutes rather than days while every individual burst stays bounded.
///
///
public int MaxPurgedRowsPerTick { get; set; } = 250_000;
///
/// Delay before the next purge tick when the previous tick stopped at
/// ; default 5 minutes. Ignored when the ceiling was not
/// reached — an ordinary tick waits .
///
///
/// Sized so the peer drains one tick's burst before the next one lands: 250 000 tombstones
/// measured at ~10 800 rows/s over a loopback pair (≈23 s), leaving an order of magnitude of
/// headroom for a real inter-node link. A worst-case 2.73 M-row backlog therefore converges
/// in ~11 ticks ≈ 55 minutes instead of 11 days at the daily interval.
///
public TimeSpan PurgeCatchUpInterval { get; set; } = TimeSpan.FromMinutes(5);
///
/// Bound on the background write queue. Default 10 000 events.
/// Overflow uses BoundedChannelFullMode.DropOldest — callers never block; the
/// dropped event's Task is faulted and FailedWriteCount is incremented
/// so the drop is observable.
///
public int WriteQueueCapacity { get; set; } = 10_000;
///
/// Widest time window a keyword search may scan; default 7 days. The keyword filter
/// is a leading-wildcard LIKE over message/source that full-scans the
/// window, so an unbounded search against a log at its storage cap walks millions of rows.
/// A keyword query with no From (or a wider range) is clamped server-side to
/// [To ?? now - window, To ?? now] rather than rejected, so existing callers keep
/// working (review 2026-10-21 02/P1).
///
public TimeSpan KeywordSearchMaxWindow { get; set; } = TimeSpan.FromDays(7);
}