namespace DodoSSH.Contracts;

/// <summary>
/// The event socket's protocol constants.
/// </summary>
/// <remarks>
/// The version lives in the subprotocol name rather than in the URL, for the reason ADR 0002 gives
/// about the rest of this API: a client and a server that upgrade independently have to agree by
/// negotiating rather than by assuming, and a WebSocket handshake already has a field for exactly
/// that. A server that does not offer <see cref="SubProtocol"/> fails the handshake, which a client
/// can act on — rather than opening a socket that then speaks a dialect it cannot read.
/// </remarks>
public static class VaultEvents
{
    /// <summary>The path the event socket is served from.</summary>
    public const string Path = "/api/v1/events";

    /// <summary>The only subprotocol this version speaks.</summary>
    public const string SubProtocol = "dodossh.events.v1";

    /// <summary>The <c>/api/v1/meta</c> feature flag advertising that this server pushes at all.</summary>
    public const string Feature = "events";

    /// <summary>
    /// Close code for a socket whose access token has expired.
    /// </summary>
    /// <remarks>
    /// In the 4000–4999 range, which the WebSocket specification reserves for applications. Its own
    /// code because it is the one close a client should answer by reconnecting immediately with a
    /// fresh token, rather than by backing off as it would for a server that went away.
    /// </remarks>
    public const int TokenExpiredCloseCode = 4401;

    /// <summary>
    /// Close code for a caller already holding as many sockets as it may.
    /// </summary>
    /// <remarks>
    /// Distinguished from <see cref="TokenExpiredCloseCode"/> because the remedy is the opposite:
    /// reconnecting at once is what caused it. A client that meets this backs off and keeps polling.
    /// </remarks>
    public const int TooManyConnectionsCloseCode = 4429;
}

/// <summary>
/// The kinds of event this socket carries.
/// </summary>
/// <remarks>
/// <para>
/// Constants rather than an <c>enum</c>, and that is a compatibility decision rather than a style
/// one. <c>DodoSshJsonContext</c> sets <c>UseStringEnumConverter</c>, which <em>throws</em> on a
/// value it does not know — so a newer server sending a kind an older client has never heard of
/// would not merely add an unreadable frame, it would break that client's socket. A string is
/// ignored instead, which is what makes this list extensible. <see cref="ProblemCodes"/> is the same
/// shape for the same reason.
/// </para>
/// <para>
/// <b>Anything a client cannot parse must be skipped, not treated as an error.</b> That rule is what
/// the shared-session frames of ADR 0012 will rely on when they arrive.
/// </para>
/// </remarks>
public static class VaultEventKinds
{
    /// <summary>The server accepted the socket. Always the first frame.</summary>
    public const string Hello = "hello";

    /// <summary>
    /// A vault has changes at or before <see cref="VaultEvent.Sequence"/>.
    /// </summary>
    /// <remarks>
    /// Carries no ciphertext and no item identity — the client's answer is the delta pull it would
    /// have run on its timer anyway. See ADR 0012 for why pushing the items themselves is refused.
    /// </remarks>
    public const string VaultChanged = "vault.changed";

    /// <summary>
    /// The set of vaults this account can reach is no longer what it was.
    /// </summary>
    /// <remarks>
    /// A vault shared with the caller, or a grant withdrawn. Deliberately says nothing about
    /// <em>which</em>: the client re-reads the list, which is the same call it already makes at the
    /// start of every synchronisation pass.
    /// </remarks>
    public const string VaultsChanged = "vaults.changed";

    /// <summary>Heartbeat. Whichever side receives one answers <see cref="Pong"/>.</summary>
    public const string Ping = "ping";

    /// <summary>The answer to a <see cref="Ping"/>.</summary>
    public const string Pong = "pong";
}

/// <summary>
/// One frame on the event socket.
/// </summary>
/// <remarks>
/// <para>
/// One flat record for every kind, with the fields a given kind does not use left null, rather than
/// a polymorphic hierarchy. The set is small, the frames are tiny, and <c>System.Text.Json</c>
/// polymorphism would put a second discriminator mechanism next to the <see cref="Kind"/> string
/// that is already the discriminator. Nothing else in <c>DodoSSH.Contracts</c> is polymorphic.
/// </para>
/// <para>
/// <b>Nothing here is secret, by construction.</b> The server cannot read a vault's contents, so a
/// notice cannot describe them; what it does disclose — that a vault changed, and when — is the same
/// metadata ADR 0001 already accepts the server holding.
/// </para>
/// </remarks>
/// <param name="Kind">One of <see cref="VaultEventKinds"/>. An unrecognised kind must be ignored.</param>
/// <param name="VaultId">The vault a <see cref="VaultEventKinds.VaultChanged"/> is about.</param>
/// <param name="Sequence">
/// The change-log position that vault has reached. A hint for logging and for coalescing, not a
/// cursor: cursors are opaque and HMAC-tagged, and this is neither.
/// </param>
/// <param name="ServerTime">
/// The server's clock when the frame was written. The client already measures skew against
/// <c>SyncPullResponse.ServerTime</c>; this lets a socket that is quiet for other reasons keep that
/// measurement current.
/// </param>
/// <param name="HeartbeatSeconds">
/// How often the server will ping, sent with <see cref="VaultEventKinds.Hello"/>. The client uses it
/// to decide when silence means the connection is dead rather than idle.
/// </param>
/// <param name="VaultCount">
/// How many vaults this socket is subscribed to, sent with <see cref="VaultEventKinds.Hello"/>.
/// Diagnostic: a socket subscribed to nothing is a real state — an account with no vaults yet — and
/// is otherwise indistinguishable from one that is quietly broken.
/// </param>
public sealed record VaultEvent(
    string Kind,
    Guid? VaultId = null,
    long? Sequence = null,
    DateTimeOffset? ServerTime = null,
    int? HeartbeatSeconds = null,
    int? VaultCount = null);
