namespace ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian;

/// <summary>
///     The historian sink contract — where qualifying alarm events land. Ingestion routes
///     through the HistorianGateway alarm writer (the gateway's <c>SendEvent</c> gRPC path)
///     behind the durable store-and-forward queue. Tests use an in-memory fake; production uses
///     <see cref="LocalDbStoreAndForwardSink"/>.
/// </summary>
/// <remarks>
///     <para>
///         <see cref="EnqueueAsync"/> is fire-and-forget from the engine's perspective —
///         the sink MUST NOT block the emitting thread. Production implementations
///         (<see cref="LocalDbStoreAndForwardSink"/>) persist to the node's local SQLite queue
///         first, then drain asynchronously to the actual historian. Per the Phase 7 plan,
///         failed downstream writes replay with exponential backoff;
///         operator actions are never blocked waiting on the historian.
///     </para>
///     <para>
///         <see cref="GetStatus"/> exposes queue depth + drain rate + last error
///         for the Admin UI <c>/alarms/historian</c> diagnostics page (Stream F).
///     </para>
/// </remarks>
public interface IAlarmHistorianSink
{
    /// <summary>Durably enqueue the event. Returns as soon as the queue row is committed.</summary>
    /// <param name="evt">The alarm historian event to enqueue.</param>
    /// <param name="cancellationToken">A cancellation token for async operations.</param>
    /// <returns>A task that represents the asynchronous operation.</returns>
    Task EnqueueAsync(AlarmHistorianEvent evt, CancellationToken cancellationToken);

    /// <summary>Snapshot of current queue depth + drain health.</summary>
    /// <returns>The current queue depth and drain health snapshot.</returns>
    HistorianSinkStatus GetStatus();
}

/// <summary>No-op default for tests or deployments that don't historize alarms.</summary>
public sealed class NullAlarmHistorianSink : IAlarmHistorianSink
{
    public static readonly NullAlarmHistorianSink Instance = new();

    /// <inheritdoc />
    public Task EnqueueAsync(AlarmHistorianEvent evt, CancellationToken cancellationToken) => Task.CompletedTask;

    /// <inheritdoc />
    public HistorianSinkStatus GetStatus() => new(
        QueueDepth: 0,
        DeadLetterDepth: 0,
        LastDrainUtc: null,
        LastSuccessUtc: null,
        LastError: null,
        DrainState: HistorianDrainState.Disabled);
}

/// <summary>Diagnostic snapshot surfaced to the Admin UI + /healthz endpoints.</summary>
/// <param name="QueueDepth">Non-dead-lettered rows waiting to be drained to the historian.</param>
/// <param name="DeadLetterDepth">Rows that have been permanently failed or have corrupt payloads; retained until the retention window expires.</param>
/// <param name="LastDrainUtc">UTC timestamp of the most recent drain attempt, or <c>null</c> if no drain has run yet.</param>
/// <param name="LastSuccessUtc">UTC timestamp of the most recent drain tick that acknowledged at least one row, or <c>null</c> if none.</param>
/// <param name="LastError">Message from the most recent writer exception or cardinality violation, cleared on the next successful batch.</param>
/// <param name="DrainState">Current state of the drain worker.</param>
/// <param name="EvictedCount">
///     Lifetime count of non-dead-lettered rows discarded because the queue reached
///     its configured capacity. Non-zero values indicate that accepted alarm events
///     were dropped before reaching the historian — operator attention required.
/// </param>
public sealed record HistorianSinkStatus(
    long QueueDepth,
    long DeadLetterDepth,
    DateTime? LastDrainUtc,
    DateTime? LastSuccessUtc,
    string? LastError,
    HistorianDrainState DrainState,
    long EvictedCount = 0);

/// <summary>Where the drain worker is in its state machine.</summary>
public enum HistorianDrainState
{
    Disabled,
    Idle,
    Draining,
    BackingOff,

    /// <summary>
    ///     Ticking, but not draining: this node does not hold the Primary role, so it leaves the
    ///     (replicated) queue for the node that does.
    /// </summary>
    /// <remarks>
    ///     Distinct from <see cref="Idle"/> so a rising queue depth on a Secondary reads as the
    ///     designed behaviour rather than a stalled drain. It is also how a misconfigured pair
    ///     becomes diagnosable: if BOTH nodes report this, no one is draining and alarm history is
    ///     silently accumulating toward the capacity ceiling.
    /// </remarks>
    NotPrimary,
}

/// <summary>Returned by the historian alarm writer per event — drain worker uses this to decide retry cadence.</summary>
public enum HistorianWriteOutcome
{
    /// <summary>Successfully persisted to the historian. Remove from queue.</summary>
    Ack,
    /// <summary>Transient failure (historian disconnected, timeout, busy). Leave queued; retry after backoff.</summary>
    RetryPlease,
    /// <summary>Permanent failure (malformed event, unrecoverable SDK error). Move to dead-letter table.</summary>
    PermanentFail,
}

/// <summary>What the drain worker delegates writes to — production is the HistorianGateway alarm writer (the gateway's <c>SendEvent</c> gRPC path).</summary>
public interface IAlarmHistorianWriter
{
    /// <summary>Push a batch of events to the historian. Returns one outcome per event, same order.</summary>
    /// <param name="batch">The batch of alarm historian events to write.</param>
    /// <param name="cancellationToken">A cancellation token for async operations.</param>
    /// <returns>One <see cref="HistorianWriteOutcome"/> per event, in the same order as <paramref name="batch"/>.</returns>
    Task<IReadOnlyList<HistorianWriteOutcome>> WriteBatchAsync(
        IReadOnlyList<AlarmHistorianEvent> batch, CancellationToken cancellationToken);
}
