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

// Physically lives under Interfaces/Services/ to match the
// established subfolder convention (REQ-COM-5b), but the namespace stays
// `ZB.MOM.WW.ScadaBridge.Commons.Interfaces` to avoid a cascading update to 9+ consumer
// files across ZB.MOM.WW.ScadaBridge.SiteRuntime, ZB.MOM.WW.ScadaBridge.AuditLog and ZB.MOM.WW.ScadaBridge.Host.
// Adopting the canonical `ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services` namespace
// can be picked up alongside any future Commons-wide namespace tidy-up.
namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces;

/// <summary>
/// Site-local source of truth for cached-operation tracking
/// (<c>ExternalSystem.CachedCall</c> / <c>Database.CachedWrite</c>) — alongside the
/// Store-and-Forward buffer, this is the row that <c>Tracking.Status(id)</c>
/// reads. One row per <see cref="TrackedOperationId"/>;
/// terminal rows are purged after a configurable retention window
/// (default 7 days).
/// </summary>
/// <remarks>
/// <para>
/// The store is intentionally a thin write-API on top of SQLite — not a
/// dispatcher. Status transitions follow
/// <c>Submitted → Retrying → Delivered / Parked / Failed / Discarded</c>; rows
/// in a terminal state never roll back. Implementations must:
/// <list type="bullet">
///   <item><description><see cref="RecordEnqueueAsync"/> is insert-if-not-exists
///   (caller-supplied id is the idempotency key — duplicate enqueues are no-ops).</description></item>
///   <item><description><see cref="RecordAttemptAsync"/> only updates non-terminal rows.</description></item>
///   <item><description><see cref="RecordTerminalAsync"/> only flips a non-terminal row to terminal.</description></item>
///   <item><description><see cref="PurgeTerminalAsync"/> deletes terminal rows whose
///   <c>TerminalAtUtc</c> is strictly older than the supplied threshold.</description></item>
/// </list>
/// </para>
/// </remarks>
public interface IOperationTrackingStore
{
    /// <summary>
    /// Insert a new tracking row in <c>Submitted</c> state with <c>RetryCount = 0</c>.
    /// Idempotent — a duplicate id is silently ignored (the existing row is left
    /// untouched), matching the at-least-once semantics of the calling site
    /// store-and-forward path.
    /// </summary>
    /// <param name="id">Unique operation ID (idempotency key).</param>
    /// <param name="kind">Kind of operation (e.g., cached call type).</param>
    /// <param name="targetSummary">Optional summary of the operation target.</param>
    /// <param name="sourceInstanceId">Optional ID of the source instance.</param>
    /// <param name="sourceScript">Optional name of the source script.</param>
    /// <param name="sourceNode">Optional source node identifier.</param>
    /// <param name="ct">Cancellation token.</param>
    /// <returns>A task that represents the asynchronous operation.</returns>
    Task RecordEnqueueAsync(
        TrackedOperationId id,
        string kind,
        string? targetSummary,
        string? sourceInstanceId,
        string? sourceScript,
        string? sourceNode,
        CancellationToken ct = default);

    /// <summary>
    /// Advance an in-flight tracking row's status, retry counter, and most-
    /// recent error/HTTP-status. Terminal rows (<see cref="RecordTerminalAsync"/>
    /// already applied) are NOT mutated — the operation has reached its final
    /// outcome and any late-arriving attempt telemetry is dropped on the floor.
    /// </summary>
    /// <param name="id">Operation ID to update.</param>
    /// <param name="status">Current operation status.</param>
    /// <param name="retryCount">Number of retry attempts.</param>
    /// <param name="lastError">Optional error message from the last attempt.</param>
    /// <param name="httpStatus">Optional HTTP status code from the last attempt.</param>
    /// <param name="ct">Cancellation token.</param>
    /// <returns>A task that represents the asynchronous operation.</returns>
    Task RecordAttemptAsync(
        TrackedOperationId id,
        string status,
        int retryCount,
        string? lastError,
        int? httpStatus,
        CancellationToken ct = default);

    /// <summary>
    /// Flip a non-terminal tracking row to terminal — sets
    /// <c>TerminalAtUtc = now</c> and writes the final status / error. A row
    /// already in terminal state is left untouched (first-write-wins).
    /// </summary>
    /// <param name="id">Operation ID to mark as terminal.</param>
    /// <param name="status">Final operation status.</param>
    /// <param name="lastError">Optional final error message.</param>
    /// <param name="httpStatus">Optional final HTTP status code.</param>
    /// <param name="ct">Cancellation token.</param>
    /// <returns>A task that represents the asynchronous operation.</returns>
    Task RecordTerminalAsync(
        TrackedOperationId id,
        string status,
        string? lastError,
        int? httpStatus,
        CancellationToken ct = default);

    /// <summary>
    /// Return the latest snapshot for the supplied id, or <c>null</c> when no
    /// tracking row exists (purged or never recorded).
    /// </summary>
    /// <param name="id">Operation ID to fetch status for.</param>
    /// <param name="ct">Cancellation token.</param>
    /// <returns>Tracking status snapshot, or null if not found.</returns>
    Task<TrackingStatusSnapshot?> GetStatusAsync(
        TrackedOperationId id,
        CancellationToken ct = default);

    /// <summary>
    /// Delete terminal rows whose <c>TerminalAtUtc</c> is strictly older than
    /// <paramref name="olderThanUtc"/>. Non-terminal rows are kept regardless
    /// of age (the operation is still in flight).
    /// </summary>
    /// <param name="olderThanUtc">Cutoff timestamp; rows terminal before this are deleted.</param>
    /// <param name="ct">Cancellation token.</param>
    /// <returns>A task that represents the asynchronous operation.</returns>
    Task PurgeTerminalAsync(
        DateTime olderThanUtc,
        CancellationToken ct = default);

    /// <summary>
    /// Reconciliation read: return tracking rows whose
    /// <c>UpdatedAtUtc</c> is at or after <paramref name="sinceUtc"/> as
    /// <see cref="SiteCallOperational"/> projections, ordered by
    /// <c>UpdatedAtUtc</c> ascending and capped at <paramref name="batchSize"/>.
    /// This is the site-side feed for central's <c>PullSiteCalls</c> RPC — the
    /// documented periodic self-heal pull that backfills the eventually-consistent
    /// central <c>SiteCalls</c> mirror when best-effort push telemetry is lost.
    /// </summary>
    /// <remarks>
    /// <para>
    /// The lower bound is inclusive so a caller can resume from the last
    /// returned <c>UpdatedAtUtc</c> without skipping a row that shares that
    /// instant; central ingest is insert-if-not-exists then upsert-on-newer, so
    /// re-reading the boundary row is a harmless no-op. The oldest-first cap lets
    /// the caller advance the cursor monotonically across follow-up pulls.
    /// </para>
    /// <para>
    /// <b>Composite keyset.</b> When <paramref name="afterId"/> is
    /// supplied the read uses a <c>(UpdatedAtUtc, TrackedOperationId)</c> keyset:
    /// it returns rows strictly after that cursor — <c>UpdatedAtUtc &gt; sinceUtc</c>,
    /// or <c>UpdatedAtUtc = sinceUtc</c> with a <c>TrackedOperationId</c> greater
    /// than <paramref name="afterId"/>. This un-pins a batch that would otherwise
    /// stall: when more than <paramref name="batchSize"/> rows share one
    /// <c>UpdatedAtUtc</c>, the plain inclusive <c>&gt;=</c> resume re-reads the
    /// same page forever. When <paramref name="afterId"/> is <c>null</c>/empty the
    /// legacy inclusive <c>&gt;=</c> contract is preserved exactly (an older
    /// central that does not send the cursor is unaffected). Ordering is always
    /// deterministic — <c>UpdatedAtUtc ASC, TrackedOperationId ASC</c>.
    /// </para>
    /// <para>
    /// <see cref="SiteCallOperational.SourceSite"/> is left as the empty string:
    /// the site id is not a tracking-store column, and the central client re-stamps
    /// it from the <c>siteId</c> it dialed (the only authority that knows which
    /// site the rows came from). <see cref="SiteCallOperational.Channel"/> is
    /// projected from the row's <c>Kind</c> (<c>DbWriteCached → DbOutbound</c>,
    /// otherwise <c>ApiOutbound</c>) and <see cref="SiteCallOperational.Target"/>
    /// from <c>TargetSummary</c>.
    /// </para>
    /// </remarks>
    /// <param name="sinceUtc">Inclusive lower bound on <c>UpdatedAtUtc</c>; <see cref="DateTime.MinValue"/> reads from the start.</param>
    /// <param name="batchSize">Maximum number of rows to return (oldest first).</param>
    /// <param name="afterId">
    /// Optional composite-keyset cursor — the <c>TrackedOperationId</c> of the last
    /// row already consumed at <paramref name="sinceUtc"/>. When supplied the read
    /// returns only rows strictly after <c>(sinceUtc, afterId)</c>; when
    /// <c>null</c>/empty the legacy inclusive <c>&gt;= sinceUtc</c> behaviour applies.
    /// </param>
    /// <param name="ct">Cancellation token.</param>
    /// <returns>The matching rows projected to <see cref="SiteCallOperational"/>, oldest-first, capped at <paramref name="batchSize"/>.</returns>
    Task<IReadOnlyList<SiteCallOperational>> ReadChangedSinceAsync(
        DateTime sinceUtc,
        int batchSize,
        string? afterId = null,
        CancellationToken ct = default);
}
