using DodoSSH.Contracts;

namespace DodoSSH.Client.Storage;

/// <summary>Why a conflict record exists.</summary>
/// <remarks>
/// Persisted, so append only. These are the vocabulary the UI reasons about: each one implies a
/// different remedy, which is why they are distinguished rather than collapsed into "conflict".
/// </remarks>
public enum ConflictKind
{
    /// <summary>Not a legal value.</summary>
    Unspecified = 0,

    /// <summary>Both sides changed a field. One value survived; the other is in the detail.</summary>
    FieldOverridden = 1,

    /// <summary>This machine deleted an item that someone else edited. The edit won.</summary>
    LocalDeleteOverridden = 2,

    /// <summary>
    /// Someone else deleted an item this machine had edited. The local content was preserved under a
    /// new id rather than being lost to the tombstone.
    /// </summary>
    RemoteDeleteResurrected = 3,

    /// <summary>
    /// A payload failed its authentication tag or its schema. Either a client bug or a server that
    /// handed back the wrong bytes; both need a human.
    /// </summary>
    Undecryptable = 4,

    /// <summary>Written by a newer client than this one, so it is readable but not editable.</summary>
    TooNewToEdit = 5,

    /// <summary>The server refused the operation outright. Retrying will not help.</summary>
    Rejected = 6,
}

/// <summary>What an offline unlock needs.</summary>
/// <param name="ServerUrl">The server this cache belongs to.</param>
/// <param name="UserId">The user, which every AAD in the bundle wrap binds to.</param>
/// <param name="Issuer">OIDC issuer.</param>
/// <param name="Subject">OIDC subject.</param>
/// <param name="Email">Email, for display.</param>
/// <param name="DisplayName">Display name, for display.</param>
/// <param name="KeyGeneration">Identity key generation.</param>
/// <param name="WrappedPrivateKey">The secret bundle, wrapped under the passphrase-derived key.</param>
/// <param name="KdfParameters">Parameters needed to re-derive that key.</param>
/// <param name="UpdatedAt">When this was last refreshed from the server.</param>
public sealed record StoredUnlockMaterial(
    string ServerUrl,
    Guid UserId,
    string Issuer,
    string Subject,
    string? Email,
    string? DisplayName,
    uint KeyGeneration,
    byte[] WrappedPrivateKey,
    KdfParameters KdfParameters,
    DateTimeOffset UpdatedAt,
    byte[]? DeviceWrappedPrivateKey = null,
    Guid? DeviceId = null);

/// <summary>A cached vault and the grant that opens it.</summary>
/// <param name="VaultId">The vault.</param>
/// <param name="Name">Display name.</param>
/// <param name="IsPersonal">Whether this is the user's personal vault.</param>
/// <param name="TeamId">Owning team, for a team vault.</param>
/// <param name="KeyGeneration">Current key generation.</param>
/// <param name="Permissions">Effective permissions, as a flags value.</param>
/// <param name="WrappedVaultKey">The vault key sealed to this user. Null while awaiting re-wrap.</param>
/// <param name="RekeyRequired">Whether a membership change has left this vault needing a rekey.</param>
/// <param name="PriorKeyWraps">
/// The same key at every generation before <paramref name="KeyGeneration"/> that this user still holds
/// a grant for. Empty for a vault that has never been rotated, and what makes one that has readable
/// back to its first item.
/// </param>
public sealed record StoredVault(
    Guid VaultId,
    string Name,
    bool IsPersonal,
    Guid? TeamId,
    uint KeyGeneration,
    int Permissions,
    byte[]? WrappedVaultKey,
    bool RekeyRequired,
    IReadOnlyList<VaultKeyWrap>? PriorKeyWraps = null)
{
    /// <summary>
    /// The <c>Write</c> bit of <see cref="Permissions"/>.
    /// </summary>
    /// <remarks>
    /// A literal rather than a reference to <c>DodoSSH.Domain.PermissionFlags</c>, because that enum is
    /// the server's and no client project references the domain assembly. The value is part of the wire
    /// contract — <c>VaultSummary.Permissions</c> is an opaque int by design — and a test pins the two
    /// together so a renumbering cannot silently make a read-only vault look writable here.
    /// </remarks>
    private const int WriteFlag = 1 << 1;

    /// <summary>
    /// Whether the server would accept a change to this vault.
    /// </summary>
    /// <remarks>
    /// A user-interface answer, not a boundary: the server checks the same bit on every push, and this
    /// exists so somebody in a team as a viewer is not offered a Save button that ends in a 403. Its
    /// counterpart — whether the items can be <em>read</em> — is not a permission at all but a question
    /// of holding the vault key, and is answered by the keyring.
    /// </remarks>
    public bool CanWrite => (Permissions & WriteFlag) == WriteFlag;
}

/// <summary>The last item state the server confirmed.</summary>
/// <param name="VaultId">Owning vault.</param>
/// <param name="EntityType">Kind of item.</param>
/// <param name="EntityId">The item.</param>
/// <param name="Version">Server-assigned version — what a push must expect.</param>
/// <param name="ChangeSequence">Position in the vault's change log.</param>
/// <param name="Payload">Ciphertext as the server returned it. Null for a tombstone.</param>
/// <param name="Fields">The plaintext columns. Null for a tombstone.</param>
/// <param name="IsDeleted">Whether this is a tombstone.</param>
/// <param name="UpdatedAt">When the change was recorded.</param>
public sealed record StoredItem(
    Guid VaultId,
    SyncEntityType EntityType,
    Guid EntityId,
    int Version,
    long ChangeSequence,
    EncryptedPayload? Payload,
    SyncPlaintextFields? Fields,
    bool IsDeleted,
    DateTimeOffset UpdatedAt);

/// <summary>The item version a pending local edit branched from.</summary>
/// <remarks>
/// Without this a conflict can only be arbitrated, not merged. It is retained verbatim, including the
/// key generation and data key id, because those are part of the payload's AAD and the ancestor cannot
/// be decrypted without them.
/// </remarks>
/// <param name="Version">The version this edit was made against.</param>
/// <param name="Payload">That version's ciphertext.</param>
/// <param name="Fields">That version's plaintext columns.</param>
public sealed record StoredAncestor(
    int Version,
    EncryptedPayload Payload,
    SyncPlaintextFields? Fields);

/// <summary>A local change waiting to be pushed.</summary>
/// <param name="Sequence">Local ordering. Assigned by the store; ignored on queue.</param>
/// <param name="OperationId">The server's idempotency key for this operation.</param>
/// <param name="VaultId">Owning vault.</param>
/// <param name="EntityType">Kind of item.</param>
/// <param name="EntityId">The item.</param>
/// <param name="Operation">Upsert or delete.</param>
/// <param name="ExpectedVersion">The version the client believes the server holds; null to create.</param>
/// <param name="Payload">Ciphertext to store. Null for a delete.</param>
/// <param name="Fields">Plaintext columns. Null for a delete.</param>
/// <param name="Ancestor">The version this branched from. Null when creating.</param>
/// <param name="QueuedAt">When the user made the change.</param>
/// <param name="Attempts">How many times this has been dispatched.</param>
/// <param name="LastError">Why it last failed.</param>
/// <param name="IsParked">Whether it has been abandoned pending user action.</param>
public sealed record PendingOperation(
    long Sequence,
    Guid OperationId,
    Guid VaultId,
    SyncEntityType EntityType,
    Guid EntityId,
    SyncOperation Operation,
    int? ExpectedVersion,
    EncryptedPayload? Payload,
    SyncPlaintextFields? Fields,
    StoredAncestor? Ancestor,
    DateTimeOffset QueuedAt,
    int Attempts = 0,
    string? LastError = null,
    bool IsParked = false);

/// <summary>Where a vault's pull has reached.</summary>
/// <param name="VaultId">The vault.</param>
/// <param name="Cursor">The last server-issued cursor. Never constructed by a client.</param>
/// <param name="KeyGeneration">The generation the server last reported.</param>
/// <param name="LastPulledAt">When the last pull completed.</param>
/// <param name="LastPushedAt">When the last push completed.</param>
/// <param name="ServerTimeSkewMs">Observed clock difference, recorded and never acted on.</param>
public sealed record StoredSyncState(
    Guid VaultId,
    string? Cursor,
    uint KeyGeneration,
    DateTimeOffset? LastPulledAt = null,
    DateTimeOffset? LastPushedAt = null,
    long ServerTimeSkewMs = 0);

/// <summary>Something the merge overrode, or an item that could not be processed.</summary>
/// <param name="Id">This record's own id, which its sealed detail is bound to.</param>
/// <param name="VaultId">Owning vault.</param>
/// <param name="EntityType">Kind of item.</param>
/// <param name="EntityId">The item.</param>
/// <param name="Kind">What happened.</param>
/// <param name="Detail">
/// The discarded values, in plaintext across this boundary and sealed at rest. Opaque to the store:
/// its shape belongs to the sync layer, which owns what a conflict means.
/// </param>
/// <param name="DetectedAt">When it was noticed.</param>
/// <param name="Acknowledged">Whether the user has dealt with it.</param>
public sealed record StoredConflict(
    Guid Id,
    Guid VaultId,
    SyncEntityType EntityType,
    Guid EntityId,
    ConflictKind Kind,
    byte[] Detail,
    DateTimeOffset DetectedAt,
    bool Acknowledged = false);
