using Microsoft.EntityFrameworkCore;

namespace DodoSSH.Client.Storage;

/// <summary>
/// Where each vault's pull has reached.
/// </summary>
/// <remarks>
/// <para>
/// The cursor is stored exactly as the server issued it and is never parsed, constructed or adjusted.
/// It is opaque and integrity-tagged for a reason: a client that could synthesise one could ask to
/// resume from a position the server never granted, and a tampered cursor is rejected rather than
/// silently mis-serving a range.
/// </para>
/// <para>
/// A null cursor means "from the beginning", which is also the recovery path for a cache that has been
/// discarded or that failed to decrypt. Re-pulling from nothing is always safe; guessing a position is
/// not.
/// </para>
/// </remarks>
public sealed class SyncStateStore(IDbContextFactory<ClientCacheContext> contexts)
{
    /// <summary>
    /// Reads a vault's position, or a fresh one starting from the beginning.
    /// </summary>
    /// <remarks>
    /// Never returns null. An unknown vault is not an error — it is a vault this client has not synced
    /// yet — and a caller forced to handle a null here would most likely handle it by starting from the
    /// beginning anyway.
    /// </remarks>
    public async Task<StoredSyncState> ReadAsync(Guid vaultId, CancellationToken cancellationToken)
    {
        var context = contexts.CreateDbContext();
        await using var scope = context.ConfigureAwait(false);

        var row = await context.Set<SyncStateRow>()
            .AsNoTracking()
            .SingleOrDefaultAsync(r => r.VaultId == vaultId, cancellationToken)
            .ConfigureAwait(false);

        return row is null
            ? new StoredSyncState(vaultId, Cursor: null, KeyGeneration: 0)
            : new StoredSyncState(
                row.VaultId,
                row.Cursor,
                row.KeyGeneration,
                row.LastPulledAtUtc,
                row.LastPushedAtUtc,
                row.ServerTimeSkewMs);
    }

    /// <summary>Records a vault's position.</summary>
    public async Task SaveAsync(StoredSyncState state, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(state);

        var context = contexts.CreateDbContext();
        await using var scope = context.ConfigureAwait(false);

        var row = await context.Set<SyncStateRow>()
            .SingleOrDefaultAsync(r => r.VaultId == state.VaultId, cancellationToken)
            .ConfigureAwait(false);

        if (row is null)
        {
            row = new SyncStateRow { VaultId = state.VaultId };
            context.Add(row);
        }

        row.Cursor = state.Cursor;
        row.KeyGeneration = state.KeyGeneration;
        row.LastPulledAtUtc = state.LastPulledAt;
        row.LastPushedAtUtc = state.LastPushedAt;
        row.ServerTimeSkewMs = state.ServerTimeSkewMs;

        await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
    }

    /// <summary>
    /// Forgets a vault's position so the next pull starts over.
    /// </summary>
    /// <remarks>
    /// <para>
    /// The remedy when the cache cannot be trusted — a key generation the client has no grant for, or
    /// rows that will not decrypt. A full re-pull is cheap next to the alternative of reasoning about
    /// which half of the cache is still valid.
    /// </para>
    /// <para>
    /// <b>The outbox is deliberately not cleared.</b> Those rows are the only copy of changes the user
    /// made and the server has not accepted; discarding them here would turn a recoverable cache
    /// problem into lost work. They re-push against the re-pulled state, conflicting and merging where
    /// they must.
    /// </para>
    /// </remarks>
    public async Task ResetAsync(Guid vaultId, CancellationToken cancellationToken)
    {
        var context = contexts.CreateDbContext();
        await using var scope = context.ConfigureAwait(false);

        await context.Set<SyncStateRow>()
            .Where(r => r.VaultId == vaultId)
            .ExecuteDeleteAsync(cancellationToken)
            .ConfigureAwait(false);

        await context.Set<CachedItemRow>()
            .Where(r => r.VaultId == vaultId)
            .ExecuteDeleteAsync(cancellationToken)
            .ConfigureAwait(false);
    }
}
