using ZB.MOM.WW.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Types;

namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;

/// <summary>
/// Site-local audit-log queue surface consumed by the site
/// <c>SiteAuditTelemetryActor</c> drain loop and the
/// <c>SiteStreamGrpcServer.PullAuditEvents</c> reconciliation handler.
/// Extracted from <c>SqliteAuditWriter</c> so both consumers can be
/// unit-tested against a stub without touching SQLite; the
/// <c>SqliteAuditWriter</c> production type implements this interface
/// and DI wires the same singleton instance to every consumer.
/// </summary>
/// <remarks>
/// Lives in Commons (rather than alongside <c>SqliteAuditWriter</c> in
/// <c>ZB.MOM.WW.ScadaBridge.AuditLog</c>) because <c>ZB.MOM.WW.ScadaBridge.Communication</c> — which
/// hosts the gRPC pull handler — must depend on this interface and
/// <c>ZB.MOM.WW.ScadaBridge.AuditLog</c> already depends on <c>ZB.MOM.WW.ScadaBridge.Communication</c>.
/// Pulling the interface up to Commons breaks the would-be cycle while
/// keeping the implementation in the AuditLog component.
///
/// Only the methods the drain and pull paths need are exposed — the
/// hot-path <c>WriteAsync</c> stays on <see cref="IAuditWriter"/>
/// (script-thread surface), separated by concern so each side can be
/// mocked independently.
/// </remarks>
public interface ISiteAuditQueue
{
    /// <summary>
    /// Returns up to <paramref name="limit"/> rows currently in
    /// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState.Pending"/>,
    /// oldest first. Idempotent — repeated calls before
    /// <see cref="MarkForwardedAsync"/> will yield the same rows again.
    /// </summary>
    /// <remarks>
    /// Cached-lifecycle audit kinds
    /// (<see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditKind.CachedSubmit"/>,
    /// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditKind.ApiCallCached"/>,
    /// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditKind.DbWriteCached"/>,
    /// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditKind.CachedResolve"/>) are
    /// EXCLUDED from this result — they ride the combined-telemetry drain via
    /// <see cref="ReadPendingCachedTelemetryAsync"/> + the central
    /// <c>OnCachedTelemetryAsync</c> dual-write transaction. The audit-only
    /// drain handled by this method covers everything else (sync ApiCall /
    /// DbWrite, NotifySend, InboundRequest, etc.).
    /// </remarks>
    /// <param name="limit">Maximum number of rows to return.</param>
    /// <param name="ct">Cancellation token.</param>
    /// <returns>A task that resolves to the oldest pending non-cached audit events, up to <paramref name="limit"/>.</returns>
    Task<IReadOnlyList<AuditEvent>> ReadPendingAsync(int limit, CancellationToken ct = default);

    /// <summary>
    /// Returns up to <paramref name="limit"/> rows in
    /// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState.Pending"/>
    /// whose audit kind belongs to the cached-call lifecycle
    /// vocabulary (<see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditKind.CachedSubmit"/>,
    /// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditKind.ApiCallCached"/>,
    /// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditKind.DbWriteCached"/>,
    /// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditKind.CachedResolve"/>),
    /// oldest first. The site-side <c>SiteAuditTelemetryActor</c> drains these
    /// rows separately, joining each with the matching operational tracking row
    /// (<c>IOperationTrackingStore.GetStatusAsync</c>) before pushing the
    /// combined <c>CachedTelemetryBatch</c> via
    /// <c>ISiteStreamAuditClient.IngestCachedTelemetryAsync</c>. Idempotent —
    /// repeated calls before <see cref="MarkForwardedAsync"/> yield the same
    /// rows again.
    /// </summary>
    /// <remarks>
    /// The two-drain partition is the production wiring of the combined-telemetry
    /// transport specified in Component-AuditLog.md §"Cached Operations —
    /// Combined Telemetry": cached rows MUST flow with their matching
    /// <c>SiteCalls</c> upsert through one MS SQL transaction at central. The
    /// prior implementation drained cached rows through the
    /// audit-only path, leaving the operational half unsent and the central
    /// dual-write handler unreachable. Returning them via this dedicated read
    /// surface lets the new drain join with the tracking store before push.
    /// </remarks>
    /// <param name="limit">Maximum number of rows to return.</param>
    /// <param name="ct">Cancellation token.</param>
    /// <returns>A task that resolves to the oldest pending cached-lifecycle audit events, up to <paramref name="limit"/>.</returns>
    Task<IReadOnlyList<AuditEvent>> ReadPendingCachedTelemetryAsync(int limit, CancellationToken ct = default);

    /// <summary>
    /// Flips the supplied EventIds from
    /// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState.Pending"/> to
    /// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState.Forwarded"/>.
    /// Non-existent or already-forwarded ids are silent no-ops.
    /// </summary>
    /// <param name="eventIds">Event IDs to mark as forwarded.</param>
    /// <param name="ct">Cancellation token.</param>
    /// <returns>A task that represents the asynchronous operation.</returns>
    Task MarkForwardedAsync(IReadOnlyList<Guid> eventIds, CancellationToken ct = default);

    /// <summary>
    /// Reconciliation-pull read surface: returns up to <paramref name="batchSize"/>
    /// rows whose <see cref="AuditEvent.OccurredAtUtc"/> &gt;= <paramref name="sinceUtc"/>
    /// and whose <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState"/> is still
    /// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState.Pending"/> or
    /// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState.Forwarded"/>.
    /// </summary>
    /// <remarks>
    /// Rows in the brief race window between site-Forwarded and central-ingest are
    /// intentionally included: the central reconciliation puller dedups on
    /// <see cref="AuditEvent.EventId"/>, so re-shipping is safe and avoids losing rows
    /// whose telemetry ack was acted on locally but never landed centrally. Ordering
    /// is oldest <see cref="AuditEvent.OccurredAtUtc"/> first with
    /// <see cref="AuditEvent.EventId"/> as the deterministic tiebreaker.
    /// </remarks>
    /// <param name="sinceUtc">Lower bound timestamp (UTC).</param>
    /// <param name="batchSize">Maximum number of rows to return.</param>
    /// <param name="afterId">
    /// Composite-keyset tiebreak cursor (WP2.3), mirroring
    /// <see cref="IOperationTrackingStore.ReadChangedSinceAsync"/>: when non-null it is the
    /// <see cref="AuditEvent.EventId"/> ("D" GUID form) of the last row already consumed at
    /// <paramref name="sinceUtc"/>, and only rows strictly after the composite
    /// <c>(OccurredAtUtc, EventId)</c> pair are returned — so a burst sharing one exact
    /// instant drains via the id tiebreak instead of pinning the inclusive-timestamp cursor.
    /// Null (the first pull, or a central that never sets it) keeps the inclusive
    /// <c>&gt;=</c> contract.
    /// </param>
    /// <param name="ct">Cancellation token.</param>
    /// <returns>A task that resolves to audit events at or after <paramref name="sinceUtc"/> in pending or forwarded state, up to <paramref name="batchSize"/>.</returns>
    Task<IReadOnlyList<AuditEvent>> ReadPendingSinceAsync(
        DateTime sinceUtc, int batchSize, string? afterId = null, CancellationToken ct = default);

    /// <summary>
    /// Reconciliation-pull commit surface, cursor form (WP2.3): flips every row at or before
    /// the composite cursor <c>(<paramref name="sinceUtc"/>, <paramref name="afterId"/>)</c>
    /// from <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState.Pending"/>/
    /// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState.Forwarded"/> to
    /// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState.Reconciled"/>.
    /// </summary>
    /// <remarks>
    /// The cursor is central's proof of receipt: it only advances past rows central has
    /// actually ingested. Flipping on THAT — rather than on having merely served the rows —
    /// is what makes the pull at-least-once: a fault between the response leaving the site
    /// and central committing it leaves the rows unflipped, so the next pull re-serves them
    /// (central dedups on <see cref="AuditEvent.EventId"/>).
    /// <para>
    /// With <paramref name="afterId"/> null the cursor is a bare timestamp under the legacy
    /// inclusive <c>&gt;=</c> read contract, so only rows STRICTLY older than
    /// <paramref name="sinceUtc"/> are proven received; rows at the boundary instant are left
    /// alone. Idempotent; already-Reconciled rows are untouched.
    /// </para>
    /// <para>
    /// <b>Only-served-rows-retire.</b> The cursor is a timestamp, and
    /// <see cref="AuditEvent.OccurredAtUtc"/> is stamped by the caller, so a row can be
    /// INSERTED after a batch was served yet carry a timestamp below the (by then advanced)
    /// cursor. Implementations MUST NOT retire such a row: it was never served, the keyset
    /// read has moved past it, and retiring it would make it purgeable — silent audit loss.
    /// The SQLite implementation bounds the flip by insertion order (<c>rowid</c> high-water
    /// mark of rows it has served), exempting rows already
    /// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState.Forwarded"/>
    /// because central ACKED those through the telemetry push path.
    /// </para>
    /// <para>
    /// <b>Liveness dependency (documented, accepted).</b> A row that WAS served but is never
    /// covered by a later cursor — central reconciliation stopped for good, or the site node
    /// restarted and its bound reset — stays
    /// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState.Pending"/>
    /// indefinitely, and <see cref="PurgeExpiredAsync"/> never purges Pending. That is the
    /// retention invariant working as designed (an unacknowledged row is not droppable), but
    /// it means the site store's floor is bounded by reconciliation actually running. The
    /// backlog is observable: <see cref="GetBacklogStatsAsync"/> reports the pending count and
    /// oldest-pending instant on every site health report, and the site reporter logs a
    /// warning once the oldest pending row exceeds its stale threshold.
    /// </para>
    /// </remarks>
    /// <param name="sinceUtc">The cursor timestamp central has consumed up to (UTC).</param>
    /// <param name="afterId">The last consumed <see cref="AuditEvent.EventId"/> at that instant, or null.</param>
    /// <param name="ct">Cancellation token.</param>
    /// <returns>A task that resolves to the number of rows flipped.</returns>
    Task<int> MarkReconciledUpToAsync(
        DateTime sinceUtc, string? afterId, CancellationToken ct = default);

    /// <summary>
    /// Reconciliation-pull commit surface: flips the supplied EventIds to
    /// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState.Reconciled"/>,
    /// but ONLY for rows currently in
    /// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState.Pending"/> or
    /// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState.Forwarded"/>.
    /// Rows already in <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState.Reconciled"/>
    /// are left untouched (idempotent re-call). Non-existent ids are silent no-ops.
    /// </summary>
    /// <param name="eventIds">Event IDs to mark as reconciled.</param>
    /// <param name="ct">Cancellation token.</param>
    /// <returns>A task that represents the asynchronous operation.</returns>
    Task MarkReconciledAsync(IReadOnlyList<Guid> eventIds, CancellationToken ct = default);

    /// <summary>
    /// Health-metric surface: returns a point-in-time snapshot
    /// of the site queue's pending count + oldest pending timestamp + on-disk
    /// SQLite file size. Surfaced on
    /// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Messages.Health.SiteHealthReport"/> as
    /// <c>SiteAuditBacklog</c> by the periodic <c>SiteAuditBacklogReporter</c>
    /// hosted service so a stuck site→central drain is visible on the central
    /// health dashboard. Safe to call concurrently with hot-path writes —
    /// implementations are expected to take the same connection lock used by
    /// the hot-path INSERT batch and the drain queries.
    /// </summary>
    /// <param name="ct">Cancellation token.</param>
    /// <returns>A task that resolves to a point-in-time snapshot of the site audit queue's pending count, oldest timestamp, and on-disk file size.</returns>
    Task<SiteAuditBacklogSnapshot> GetBacklogStatsAsync(CancellationToken ct = default);

    /// <summary>
    /// Retention purge: permanently deletes rows that have already left the site
    /// (<see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState.Forwarded"/> or
    /// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState.Reconciled"/>)
    /// whose <see cref="AuditEvent.OccurredAtUtc"/> is strictly older than
    /// <paramref name="olderThanUtc"/>, and reclaims the freed pages.
    /// </summary>
    /// <remarks>
    /// <b>Hard <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState"/> invariant:</b>
    /// a row still in <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState.Pending"/>
    /// is NEVER purged on age alone — it has not yet reached central, so dropping it
    /// would silently lose audit data. Only Forwarded/Reconciled rows are eligible.
    /// Idempotent: a second call with the same cutoff removes nothing further and
    /// returns 0. The site store is ephemeral (~7-day retention), so this bounds its
    /// unbounded growth (arch-review 04, S1/U2).
    /// </remarks>
    /// <param name="olderThanUtc">Exclusive upper bound on <see cref="AuditEvent.OccurredAtUtc"/>; rows at or after this instant are retained.</param>
    /// <param name="ct">Cancellation token.</param>
    /// <returns>A task that resolves to the number of canonical rows deleted.</returns>
    Task<int> PurgeExpiredAsync(DateTime olderThanUtc, CancellationToken ct = default);
}
