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

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

/// <summary>
/// Append-only data access for the central <c>AuditLog</c> table (Audit Log #23).
/// </summary>
/// <remarks>
/// <para>
/// The append-only invariant is enforced both at the SQL level (the
/// <c>scadabridge_audit_writer</c> role has only INSERT + SELECT — UPDATE and DELETE
/// are not granted) and at the API level: this interface deliberately exposes no
/// Update and no single-row Delete. Bulk purge is performed exclusively via
/// monthly partition switch-out (<see cref="SwitchOutPartitionAsync"/>).
/// </para>
/// <para>
/// Ingest is idempotent on <c>EventId</c>: <see cref="InsertIfNotExistsAsync"/> is
/// first-write-wins, so retrying telemetry and reconciliation pulls can both feed
/// the same writer without producing duplicates.
/// </para>
/// </remarks>
public interface IAuditLogRepository
{
    /// <summary>
    /// Inserts <paramref name="evt"/> if no row with the same
    /// <see cref="AuditEvent.EventId"/> exists; otherwise silently leaves the
    /// stored row untouched (first-write-wins). Bypasses the EF change tracker
    /// so the row never enters a tracked state.
    /// </summary>
    /// <param name="evt">The audit event to insert.</param>
    /// <param name="ct">Cancellation token.</param>
    /// <returns>A task that represents the asynchronous operation.</returns>
    Task InsertIfNotExistsAsync(AuditEvent evt, CancellationToken ct = default);

    /// <summary>
    /// Returns up to <see cref="AuditLogPaging.PageSize"/> rows matching
    /// <paramref name="filter"/>, ordered by <c>(OccurredAtUtc DESC, EventId DESC)</c>.
    /// Use keyset paging by passing the last returned row's
    /// <c>OccurredAtUtc</c> + <c>EventId</c> back via
    /// <see cref="AuditLogPaging.AfterOccurredAtUtc"/> +
    /// <see cref="AuditLogPaging.AfterEventId"/> to fetch the next page.
    /// </summary>
    /// <param name="filter">Filter criteria to apply to the query.</param>
    /// <param name="paging">Paging cursor and page size.</param>
    /// <param name="ct">Cancellation token.</param>
    /// <returns>A task that resolves to the matching audit events for the requested page, ordered by <c>(OccurredAtUtc DESC, EventId DESC)</c>.</returns>
    Task<IReadOnlyList<AuditEvent>> QueryAsync(
        AuditLogQueryFilter filter,
        AuditLogPaging paging,
        CancellationToken ct = default);

    /// <summary>
    /// Switches out (purges) the monthly partition whose lower bound is
    /// <paramref name="monthBoundary"/> and returns the approximate number
    /// of rows discarded — sampled inside the transaction BEFORE the switch
    /// so the row count reflects what the switch removed, not a post-purge
    /// scan of a table that no longer exists.
    /// </summary>
    /// <remarks>
    /// <para>
    /// <b>Drop-and-rebuild dance.</b> <c>UX_AuditLog_EventId</c> is intentionally
    /// non-partition-aligned (it lives on <c>[PRIMARY]</c> so single-column
    /// EventId uniqueness — required by <see cref="InsertIfNotExistsAsync"/> —
    /// can be enforced cheaply). SQL Server rejects
    /// <c>ALTER TABLE … SWITCH PARTITION</c> while a non-aligned unique index
    /// is present, so the M6 implementation drops the index, creates a staging
    /// table with byte-identical schema, switches the partition's data into
    /// staging, drops staging (discarding the rows), and rebuilds the unique
    /// index. The CATCH branch guarantees the index is rebuilt even on partial
    /// failure so the table never returns to live traffic without its
    /// idempotency-supporting index.
    /// </para>
    /// <para>
    /// <b>Outage window.</b> The dance briefly removes the unique index, so
    /// concurrent <see cref="InsertIfNotExistsAsync"/> calls during the switch
    /// could in principle race past the IF NOT EXISTS check without the index
    /// catching the duplicate. This is acceptable for the daily purge cadence
    /// — the inserts that the IF NOT EXISTS check guards are themselves rare
    /// enough that a sub-second collision window is operationally negligible,
    /// and the composite PK still rejects same-(EventId, OccurredAtUtc) rows.
    /// </para>
    /// </remarks>
    /// <param name="monthBoundary">Lower-bound datetime of the monthly partition to switch out.</param>
    /// <param name="ct">Cancellation token.</param>
    /// <returns>A task that resolves to the approximate number of rows discarded by the partition switch.</returns>
    Task<long> SwitchOutPartitionAsync(DateTime monthBoundary, CancellationToken ct = default);

    /// <summary>
    /// Returns the set of <c>pf_AuditLog_Month</c> partition lower-bound
    /// boundaries whose partitions contain only rows with
    /// <see cref="AuditEvent.OccurredAtUtc"/> strictly older than
    /// <paramref name="threshold"/>. Boundaries whose partition is empty are
    /// excluded (a no-op switch is wasted work). Used by the M6 purge actor
    /// to enumerate retention-eligible months on every tick.
    /// </summary>
    /// <param name="threshold">Only partitions whose data is entirely older than this UTC datetime are returned.</param>
    /// <param name="ct">Cancellation token.</param>
    /// <returns>A task that resolves to the list of partition lower-bound boundaries eligible for purge.</returns>
    Task<IReadOnlyList<DateTime>> GetPartitionBoundariesOlderThanAsync(
        DateTime threshold,
        CancellationToken ct = default);

    /// <summary>
    /// Audit Log (#23) M7 Bundle E (T13) — returns aggregate counts over the
    /// trailing <paramref name="window"/> driving the central Health
    /// dashboard's Audit KPI tiles.
    /// </summary>
    /// <param name="window">
    /// Trailing time window (e.g. <c>TimeSpan.FromHours(1)</c>). Rows whose
    /// <c>OccurredAtUtc &gt;= nowUtc - window</c> are counted; the upper
    /// bound is <paramref name="nowUtc"/>.
    /// </param>
    /// <param name="nowUtc">
    /// Optional explicit "now" timestamp used to anchor the trailing window.
    /// Defaults to <see cref="DateTime.UtcNow"/> at call time when null —
    /// production callers should leave this null; tests pin a deterministic
    /// value so the window is reproducible across runs.
    /// </param>
    /// <param name="ct">Cancellation token.</param>
    /// <returns>
    /// A snapshot with <c>TotalEventsLastHour</c> + <c>ErrorEventsLastHour</c>
    /// populated; <c>BacklogTotal</c> is left at zero (this method has no
    /// visibility into per-site backlogs — the service layer composes it in
    /// from <see cref="ZB.MOM.WW.ScadaBridge.HealthMonitoring.ICentralHealthAggregator"/>).
    /// <c>AsOfUtc</c> is set to the server-side <c>UtcNow</c> at the time of
    /// the query.
    /// </returns>
    /// <remarks>
    /// <para>
    /// Implemented as a single aggregate query
    /// (<c>SELECT COUNT_BIG(*) AS Total, SUM(CASE …) AS Errors</c>) rather than
    /// two round trips so the volume + error rate tiles read a consistent
    /// snapshot — the denominator and numerator come from the same scan.
    /// </para>
    /// <para>
    /// Errors are defined as <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditStatus.Failed"/>,
    /// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditStatus.Parked"/>, or
    /// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditStatus.Discarded"/>
    /// — every non-success terminal lifecycle state. <c>Submitted</c>,
    /// <c>Forwarded</c>, <c>Attempted</c> are in-flight and are NOT errors;
    /// <c>Delivered</c> is success; <c>Skipped</c> is an intentional no-op.
    /// </para>
    /// </remarks>
    Task<AuditLogKpiSnapshot> GetKpiSnapshotAsync(
        TimeSpan window,
        DateTime? nowUtc = null,
        CancellationToken ct = default);

    /// <summary>
    /// Audit Log ParentExecutionId feature (Task 8) — given any
    /// <paramref name="executionId"/> in an execution chain, returns the whole
    /// chain rooted at the topmost ancestor: one <see cref="ExecutionTreeNode"/>
    /// per distinct execution, summarising its <c>AuditLog</c> rows. The Central
    /// UI renders the result as a tree.
    /// </summary>
    /// <remarks>
    /// <para>
    /// The input id may be any node in the chain — a leaf, the root, or a middle
    /// node. The implementation first walks <em>up</em> via
    /// <c>ParentExecutionId</c> to find the root, then walks <em>down</em> from
    /// the root via a recursive CTE, so the full chain is returned regardless of
    /// entry point.
    /// </para>
    /// <para>
    /// The <c>ParentExecutionId</c> graph is a tree (acyclic by construction —
    /// each execution is minted fresh and its parent always pre-exists). Both
    /// the upward walk and the downward CTE are nonetheless bounded at 32 levels
    /// as a guard against corrupt/pathological data: a depth that exceeds the
    /// guard raises an error rather than hanging the server. Chains are shallow
    /// (1-2 levels typical) so the guard is never reached in practice.
    /// </para>
    /// <para>
    /// A "stub" node — an execution that emitted no rows of its own yet is
    /// referenced by a child via <c>ParentExecutionId</c>, or whose rows have
    /// been purged — still appears, with <see cref="ExecutionTreeNode.RowCount"/>
    /// = 0. A purged/missing parent simply ends the upward walk.
    /// </para>
    /// <para>
    /// When no <c>AuditLog</c> row carries <paramref name="executionId"/> in
    /// either <c>ExecutionId</c> or <c>ParentExecutionId</c>, the result is a
    /// single stub node for <paramref name="executionId"/> itself
    /// (<see cref="ExecutionTreeNode.RowCount"/> = 0) — consistent with the
    /// stub-node treatment of any other row-less execution.
    /// </para>
    /// </remarks>
    /// <param name="executionId">Any execution id in the chain; the implementation walks to the root and back down.</param>
    /// <param name="ct">Cancellation token.</param>
    /// <returns>A task that resolves to the full execution tree rooted at the topmost ancestor, one node per distinct execution.</returns>
    Task<IReadOnlyList<ExecutionTreeNode>> GetExecutionTreeAsync(
        Guid executionId,
        CancellationToken ct = default);

    /// <summary>
    /// Returns the distinct, non-null <c>SourceNode</c> values present in the
    /// <c>AuditLog</c> table, in ascending order. Backs the Audit Log page's
    /// "Node" multi-select filter dropdown — the Central UI caches the result
    /// for ~60s so the repository is hit at most once per minute per circuit.
    /// </summary>
    /// <param name="ct">Cancellation token.</param>
    /// <returns>A task that resolves to the distinct, non-null source node names in ascending order.</returns>
    Task<IReadOnlyList<string>> GetDistinctSourceNodesAsync(CancellationToken ct = default);
}
