using DodoSSH.Client.Domain;
using DodoSSH.Client.Sync;

namespace DodoSSH.Client.Session;

/// <summary>How much log a vault keeps.</summary>
/// <param name="MaxAge">How far back entries are kept.</param>
/// <param name="MaxEntries">How many entries of each kind are kept, whatever their age.</param>
/// <remarks>
/// <para>
/// Two limits rather than one, and whichever bites first wins. An age alone lets somebody who connects two
/// hundred times a day accumulate a log nobody wants to sync; a count alone means a quiet month of work
/// disappears the week somebody has a busy afternoon.
/// </para>
/// <para>
/// <b>Retention is not optional here the way it is for a local log file.</b> These entries sync, so keeping
/// them for ever costs every machine in the vault the bandwidth and the storage — which is the price of the
/// decision that made them auditable in the first place.
/// </para>
/// </remarks>
public sealed record LogRetention(TimeSpan MaxAge, int MaxEntries)
{
    /// <summary>Ninety days, or five thousand entries of each kind.</summary>
    public static LogRetention Default { get; } = new(TimeSpan.FromDays(90), 5_000);
}

/// <summary>What one pruning pass removed.</summary>
/// <param name="Connections">Connection entries deleted.</param>
/// <param name="Activity">Activity entries deleted.</param>
public sealed record LogPruneResult(int Connections, int Activity)
{
    /// <summary>Whether anything went.</summary>
    public bool RemovedAnything => Connections > 0 || Activity > 0;
}

/// <summary>
/// Removes log entries a vault has agreed to stop keeping.
/// </summary>
/// <remarks>
/// <para>
/// <b>A real tombstone delete that pushes</b>, because these are synced items — so pruning is not a local
/// tidy-up and cannot be run on a whim. It goes once when a vault opens and at most once per auto-sync tick
/// behind a last-pruned stamp; the alternative, a timer of its own, would be a second thing waking a laptop
/// up to write to a server.
/// </para>
/// <para>
/// <b>Age is read from the entry, not from the item.</b> A connection entry knows when the connection
/// started and an activity entry knows when the change happened, and both are the times a person means. The
/// item id's own v7 timestamp is close but not the same — it is when the entry was <em>written</em>, which
/// for a connection is when it ended.
/// </para>
/// </remarks>
public static class LogPruner
{
    /// <summary>Deletes whatever falls outside the retention policy.</summary>
    /// <param name="session">The open vault.</param>
    /// <param name="retention">What to keep.</param>
    /// <param name="now">The moment to measure age from.</param>
    /// <param name="cancellationToken">Cancellation.</param>
    /// <remarks>
    /// Reads both logs in full, which is what makes the count limit possible at all: neither the server nor
    /// the local mirror can order encrypted entries, so the only place that can decide which five thousand
    /// to keep is a client that has decrypted them.
    /// </remarks>
    public static async Task<LogPruneResult> PruneAsync(
        VaultSession session,
        LogRetention retention,
        DateTimeOffset now,
        CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(session);
        ArgumentNullException.ThrowIfNull(retention);

        var connections = await session.ConnectionLog
            .ListAsync(session.ActiveVaultId, cancellationToken)
            .ConfigureAwait(false);

        var activity = await session.ActivityLog
            .ListAsync(session.ActiveVaultId, cancellationToken)
            .ConfigureAwait(false);

        var cutoff = now - retention.MaxAge;

        var staleConnections = Stale(
            connections.Items, retention, cutoff, entry => entry.Secret.StartedAt);

        var staleActivity = Stale(activity.Items, retention, cutoff, entry => entry.Secret.At);

        foreach (var entry in staleConnections)
        {
            await session.ConnectionLog
                .DeleteAsync(session.ActiveVaultId, entry, cancellationToken)
                .ConfigureAwait(false);
        }

        foreach (var entry in staleActivity)
        {
            await session.ActivityLog
                .DeleteAsync(session.ActiveVaultId, entry, cancellationToken)
                .ConfigureAwait(false);
        }

        return new LogPruneResult(staleConnections.Count, staleActivity.Count);
    }

    /// <summary>The ids of the entries that fall outside the policy, newest kept.</summary>
    private static IReadOnlyList<Guid> Stale<TSecret>(
        IReadOnlyList<VaultItem<TSecret>> entries,
        LogRetention retention,
        DateTimeOffset cutoff,
        Func<VaultItem<TSecret>, DateTimeOffset> at)
        where TSecret : class, IVaultSecret
    {
        var ordered = entries.OrderByDescending(at).ToArray();

        return
        [
            .. ordered
                .Where((entry, index) => index >= retention.MaxEntries || at(entry) < cutoff)
                .Select(entry => entry.EntityId),
        ];
    }
}
