using DodoSSH.Contracts;
using DodoSSH.Crypto;
using DodoSSH.Infrastructure;
using Microsoft.EntityFrameworkCore;

namespace DodoSSH.Api.Features.Identity;

/// <summary>
/// Serves the append-only key log.
/// </summary>
/// <remarks>
/// <para>
/// Readable by every enrolled caller, in full. That is the point of it: a log only one party can read
/// proves nothing, and the whole mechanism is that independent clients compare what they were shown.
/// Nothing here is secret — public keys, signatures over them, and hashes.
/// </para>
/// <para>
/// The server never edits this table and this service never writes to it. Appends happen in exactly
/// one place, under a deployment-wide advisory lock, inside the enrollment transaction; see
/// <see cref="EnrollmentService"/> and docs/crypto.md §7.2 for why serialising them is load-bearing.
/// </para>
/// </remarks>
internal sealed class KeyLogService(DodoDbContext database)
{
    /// <summary>Largest page served, whatever a caller asks for.</summary>
    /// <remarks>
    /// A client verifying the chain has to read every entry in order, so paging is a transfer-size
    /// concern rather than a filter. The cap is generous because skipping entries is not an option:
    /// a gap breaks the link and the verification fails, correctly, on data that was fine.
    /// </remarks>
    private const int MaxPageSize = 500;

    /// <summary>Reads entries after a sequence, with the log's current head.</summary>
    internal async Task<KeyLogPage> ReadAsync(
        long afterSequence,
        int? limit,
        CancellationToken cancellationToken)
    {
        var take = Math.Clamp(limit ?? MaxPageSize, 1, MaxPageSize);

        var entries = await database.KeyLog
            .Where(e => e.Sequence > afterSequence)
            .OrderBy(e => e.Sequence)

            // One more than asked for, so "is there another page" is answered by what came back
            // rather than by a second count that could disagree with it.
            .Take(take + 1)
            .ToListAsync(cancellationToken)
            .ConfigureAwait(false);

        var hasMore = entries.Count > take;

        if (hasMore)
        {
            entries.RemoveAt(entries.Count - 1);
        }

        var head = await database.KeyLog
            .OrderByDescending(e => e.Sequence)
            .Select(e => new { e.Sequence, e.Hash })
            .FirstOrDefaultAsync(cancellationToken)
            .ConfigureAwait(false);

        return new KeyLogPage(
            Entries:
            [
                .. entries.Select(e => new KeyLogRecord(
                    e.Sequence,
                    e.UserId,
                    e.Generation,
                    e.EncryptionPublicKey,
                    e.SigningPublicKey,
                    e.StatementSignature,
                    e.PreviousHash,
                    e.Hash,
                    e.CreatedAtUtc)),
            ],

            HeadSequence: head?.Sequence ?? 0,

            // The genesis predecessor for an empty log, which is the same value the first entry will
            // record. A client comparing heads therefore needs no special case for "nothing yet".
            Head: head?.Hash ?? KeyLogChain.CreateGenesisPreviousHash(),

            HasMore: hasMore);
    }
}
