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

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

/// <summary>
/// Data access for the central notification outbox — the queue of <see cref="Notification"/>
/// rows the outbox actor drains, retries, and audits. Distinct from
/// <see cref="INotificationRepository"/>, which manages notification list configuration.
/// </summary>
/// <remarks>
/// Persistence model: <see cref="InsertIfNotExistsAsync"/> and <see cref="UpdateAsync"/> commit
/// internally, so each call is its own transaction — suited to the outbox actor committing one
/// row's status transition at a time. The standalone <see cref="SaveChangesAsync"/> is available
/// for callers that stage multiple changes and want to flush them together.
/// </remarks>
public interface INotificationOutboxRepository
{
    /// <summary>
    /// Inserts <paramref name="n"/> only if no row with the same
    /// <see cref="Notification.NotificationId"/> exists. Returns <c>true</c> when a new
    /// row was inserted, <c>false</c> when an existing row was left untouched.
    /// Commits internally — this call is its own transaction.
    /// </summary>
    /// <param name="n">The notification to insert.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>True if inserted, false if already exists.</returns>
    Task<bool> InsertIfNotExistsAsync(Notification n, CancellationToken cancellationToken = default);

    /// <summary>
    /// Returns notifications ready for a delivery attempt: <c>Pending</c> rows, plus
    /// <c>Retrying</c> rows whose <c>NextAttemptAt</c> is at or before <paramref name="now"/>.
    /// Terminal rows are excluded. Ordered by <c>CreatedAt</c> ascending, capped at
    /// <paramref name="batchSize"/>.
    /// </summary>
    /// <param name="now">The current time for evaluating due retries.</param>
    /// <param name="batchSize">Maximum number of rows to return.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>A list of notifications ready for delivery.</returns>
    Task<IReadOnlyList<Notification>> GetDueAsync(DateTimeOffset now, int batchSize, CancellationToken cancellationToken = default);

    /// <summary>Returns the notification with the given id, or <c>null</c>.</summary>
    /// <param name="notificationId">The notification identifier.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The notification, or null if not found.</returns>
    Task<Notification?> GetByIdAsync(string notificationId, CancellationToken cancellationToken = default);

    /// <summary>
    /// Persists <paramref name="n"/>'s <b>delivery-state</b> columns —
    /// <c>Status</c>, <c>RetryCount</c>, <c>LastError</c>, <c>ResolvedTargets</c>,
    /// <c>LastAttemptAt</c>, <c>NextAttemptAt</c>, <c>DeliveredAt</c>. Commits
    /// internally — this call is its own transaction.
    /// </summary>
    /// <remarks>
    /// <b>Scope is deliberately narrow.</b> Those seven columns are the ONLY
    /// mutable state a notification has: everything else (identity, type, list,
    /// subject, body, type data, source/origin attribution, enqueue and creation
    /// timestamps) is written once at ingest and is immutable by contract. Every
    /// caller — the dispatcher's per-attempt write and the operator retry/discard
    /// one-shots — touches only this set. Implementations are therefore free to
    /// issue a targeted UPDATE of these columns rather than rewriting the whole
    /// row, which matters because the row carries <c>nvarchar(max)</c> body and
    /// payload columns and the dispatcher writes on EVERY attempt. A future
    /// caller that needs to change an immutable column must add its own method
    /// rather than widening this one.
    /// <para>
    /// <b>The return value is the not-found signal.</b> A targeted UPDATE against
    /// a row that no longer exists (the retention purge removed it between the
    /// read and the write) affects zero rows and is otherwise indistinguishable
    /// from success — which is how an operator Retry/Discard came to report
    /// success against a vanished notification. Implementations MUST return
    /// <see langword="false"/> when no row matched. Callers that speak to a human
    /// (the operator one-shots) must surface that as "not found"; the dispatcher
    /// treats it as a lost race and logs.
    /// </para>
    /// </remarks>
    /// <param name="n">The notification whose delivery state should be persisted.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>
    /// <see langword="true"/> when the row was found and its delivery state
    /// persisted; <see langword="false"/> when no row with that
    /// <c>NotificationId</c> exists any more.
    /// </returns>
    Task<bool> UpdateAsync(Notification n, CancellationToken cancellationToken = default);

    /// <summary>
    /// Returns a page of notifications matching <paramref name="filter"/>, ordered by
    /// <c>CreatedAt</c> descending, together with the total matching count.
    /// </summary>
    /// <param name="filter">The query filter.</param>
    /// <param name="pageNumber">The page number (1-based).</param>
    /// <param name="pageSize">The page size.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>A tuple of rows and total count.</returns>
    Task<(IReadOnlyList<Notification> Rows, int TotalCount)> QueryAsync(
        NotificationOutboxFilter filter, int pageNumber, int pageSize, CancellationToken cancellationToken = default);

    /// <summary>
    /// Bulk-deletes terminal rows (Delivered/Parked/Discarded) whose <c>CreatedAt</c> is
    /// older than <paramref name="cutoff"/>. Returns the number of rows deleted.
    /// </summary>
    /// <param name="cutoff">The cutoff time for deletion.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The number of rows deleted.</returns>
    Task<int> DeleteTerminalOlderThanAsync(DateTimeOffset cutoff, CancellationToken cancellationToken = default);

    /// <summary>
    /// Computes a point-in-time <see cref="NotificationKpiSnapshot"/>. The stuck and
    /// delivered cutoffs are supplied by the caller; the current time used for
    /// <c>OldestPendingAge</c> is captured inside the method.
    /// </summary>
    /// <param name="stuckCutoff">The time threshold for marking notifications as stuck.</param>
    /// <param name="deliveredSince">The time threshold for counting delivered notifications.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>A KPI snapshot.</returns>
    Task<NotificationKpiSnapshot> ComputeKpisAsync(
        DateTimeOffset stuckCutoff, DateTimeOffset deliveredSince, CancellationToken cancellationToken = default);

    /// <summary>
    /// Computes a point-in-time <see cref="SiteNotificationKpiSnapshot"/> per source site.
    /// Sites with no notification rows at all are omitted. The stuck and delivered cutoffs
    /// are supplied by the caller; the current time used for <c>OldestPendingAge</c> is
    /// captured inside the method.
    /// </summary>
    /// <param name="stuckCutoff">The time threshold for marking notifications as stuck.</param>
    /// <param name="deliveredSince">The time threshold for counting delivered notifications.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>A list of per-site KPI snapshots.</returns>
    Task<IReadOnlyList<SiteNotificationKpiSnapshot>> ComputePerSiteKpisAsync(
        DateTimeOffset stuckCutoff, DateTimeOffset deliveredSince, CancellationToken cancellationToken = default);

    /// <summary>
    /// Computes a point-in-time <see cref="NodeNotificationKpiSnapshot"/> per originating node.
    /// Nodes with no notification rows at all are omitted; rows with a <c>NULL</c>
    /// <c>SourceNode</c> are excluded. The stuck and delivered cutoffs are supplied by the
    /// caller; the current time used for <c>OldestPendingAge</c> is captured inside the method.
    /// </summary>
    /// <param name="stuckCutoff">The time threshold for marking notifications as stuck.</param>
    /// <param name="deliveredSince">The time threshold for counting delivered notifications.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>A list of per-node KPI snapshots, ordered by node name.</returns>
    Task<IReadOnlyList<NodeNotificationKpiSnapshot>> ComputePerNodeKpisAsync(
        DateTimeOffset stuckCutoff, DateTimeOffset deliveredSince, CancellationToken cancellationToken = default);

    /// <summary>
    /// Persists pending changes tracked on the underlying context. Use this when staging
    /// multiple changes for a single commit; the individual mutating methods on this
    /// interface already commit on their own.
    /// </summary>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The number of changes persisted.</returns>
    Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}
