using DodoSSH.Contracts;

namespace DodoSSH.Client.Storage;

/// <summary>
/// What unlock needs, and nothing else.
/// </summary>
/// <remarks>
/// <para>
/// A single row: <see cref="Id"/> is always <see cref="SingletonId"/>. One cache database holds one
/// server and one user. Multiple accounts are a real feature and they deserve their own design —
/// which server a vault came from, which identity signed a grant, which profile a window belongs to
/// — rather than a half-provision now that would have to be undone.
/// </para>
/// <para>
/// This is the row that makes an offline launch work. The KDF salt and the wrapped bundle are cached
/// here precisely so that unlock needs no network: fetching a salt at unlock time would mean the
/// vault cannot be opened on a plane, which is the most common moment a user needs it. Neither is a
/// secret — the salt is public by design and the bundle is ciphertext.
/// </para>
/// </remarks>
internal sealed class UnlockMaterialRow
{
    /// <summary>The only legal primary key.</summary>
    internal const int SingletonId = 1;

    public int Id { get; set; } = SingletonId;

    public string ServerUrl { get; set; } = string.Empty;

    public Guid UserId { get; set; }

    public string Issuer { get; set; } = string.Empty;

    public string Subject { get; set; } = string.Empty;

    public string? Email { get; set; }

    public string? DisplayName { get; set; }

    public uint KeyGeneration { get; set; }

    /// <summary>The secret bundle, wrapped under the passphrase-derived key. Ciphertext.</summary>
    public byte[] WrappedPrivateKey { get; set; } = [];

    /// <summary>
    /// The same bundle sealed to this machine's device key, when one is registered. Ciphertext.
    /// </summary>
    /// <remarks>
    /// Cached for the same reason the passphrase wrap is: so unlock needs no network. Fetching it at
    /// unlock time would mean the one unlock path that exists to save the user typing only works when
    /// they are online, which is backwards — a laptop on a plane is exactly where this should help.
    /// <para>
    /// Holding it here is what makes whatever guards the device key as strong as the passphrase for this
    /// cache, which is stated in ADR 0007 rather than hidden in a column comment.
    /// </para>
    /// </remarks>
    public byte[]? DeviceWrappedPrivateKey { get; set; }

    /// <summary>The server's id for this device, so its wrap can be revoked.</summary>
    /// <remarks>
    /// Kept because forgetting a device locally is only half the job: the server's row outlives it and
    /// would go on claiming this machine can unlock. See ADR 0007's consequences.
    /// </remarks>
    public Guid? DeviceId { get; set; }

    public string KdfAlgorithm { get; set; } = string.Empty;

    public byte[] KdfSalt { get; set; } = [];

    /// <summary>Kibibytes, matching both libsodium and the storage column on the server.</summary>
    public int KdfMemoryKibibytes { get; set; }

    public int KdfPasses { get; set; }

    public int KdfParallelism { get; set; }

    public DateTimeOffset UpdatedAtUtc { get; set; }
}

/// <summary>
/// The sign-in this machine may resume without opening a browser.
/// </summary>
/// <remarks>
/// <para>
/// A single row, like <see cref="UnlockMaterialRow"/> and for the same reason: one cache holds one
/// account.
/// </para>
/// <para>
/// <b>The token is sealed under the LocalCacheKey, which is the whole point of storing it here.</b> A
/// refresh token is a long-lived credential for the account — not for the vault, which nothing but the
/// passphrase opens — so a copy of this file lifted off a stolen laptop must not be one. Sealing it under
/// a key that exists only while the vault is unlocked means the sign-in can only be resumed by somebody
/// who has already opened the vault, which is exactly the moment the application wants it: unlock, then
/// come back online by itself. It also means a locked machine cannot reach the server at all, which is a
/// consequence worth stating rather than a limitation to work around.
/// </para>
/// </remarks>
internal sealed class RememberedSignInRow
{
    /// <summary>The only legal primary key.</summary>
    internal const int SingletonId = 1;

    public int Id { get; set; } = SingletonId;

    /// <summary>The refresh token, sealed under the LocalCacheKey. Ciphertext.</summary>
    public byte[] SealedRefreshToken { get; set; } = [];

    public DateTimeOffset UpdatedAtUtc { get; set; }
}

/// <summary>A vault the user can reach, with the grant that opens it.</summary>
/// <remarks>
/// Cached so the vault list and the key needed to decrypt it are both available offline. The name is
/// plaintext here for the same reason it is plaintext on the server: a user has to pick a vault
/// before anything has been decrypted.
/// </remarks>
internal sealed class CachedVaultRow
{
    public Guid VaultId { get; set; }

    public string Name { get; set; } = string.Empty;

    public bool IsPersonal { get; set; }

    public Guid? TeamId { get; set; }

    public uint KeyGeneration { get; set; }

    public int Permissions { get; set; }

    /// <summary>
    /// The vault key sealed to this user's X25519 key. Null while a grant awaits re-wrap after a
    /// rekey, in which case the vault is temporarily unreadable.
    /// </summary>
    public byte[]? WrappedVaultKey { get; set; }

    public bool RekeyRequired { get; set; }

    /// <summary>
    /// Whether this machine has been asked to leave this vault's items off the screens that show them.
    /// </summary>
    /// <remarks>
    /// <para>
    /// A reading preference and nothing more. It is read by the view models that build the lists a
    /// person looks at, and by nothing else — never by sync, never by the keyring, and never by the
    /// trust the SSH handshake consults. A vault hidden here still syncs, still opens, and still
    /// answers when a shown host authenticates with a key that lives in it.
    /// </para>
    /// <para>
    /// Here rather than in <c>ClientSettings</c> because it is keyed by vault id, and that file is
    /// plaintext in the profile directory with a stated rule about what may go in it. This one is also
    /// per machine — a vault you have set aside on a laptop is not one you have set aside on a phone —
    /// which is why it never reaches the server and is not part of <see cref="StoredVault"/>.
    /// </para>
    /// </remarks>
    public bool Hidden { get; set; }

    public DateTimeOffset UpdatedAtUtc { get; set; }
}

/// <summary>
/// A vault key this user holds for a generation the vault has moved past.
/// </summary>
/// <remarks>
/// <para>
/// A table rather than a column, because there is one of these per rotation and the vault row has one
/// of everything else. The current generation's wrap stays on <see cref="CachedVaultRow"/>: it is what
/// unlocking needs, and burying it in a child table would make the common case the awkward one.
/// </para>
/// <para>
/// Cached for the reason the current wrap is. An item keeps the generation it was sealed under, so a
/// machine that came back from a rotation with only the newest key would read everything written
/// before it as corrupt — offline, with no way to ask for the rest.
/// </para>
/// </remarks>
internal sealed class CachedVaultKeyWrapRow
{
    public Guid VaultId { get; set; }

    public uint KeyGeneration { get; set; }

    /// <summary>The vault key at this generation, sealed to this user's X25519 key.</summary>
    public byte[] WrappedKey { get; set; } = [];
}

/// <summary>
/// The last state of an item that the server confirmed.
/// </summary>
/// <remarks>
/// <para>
/// Strictly a mirror: this row is what the server said, never what the user has typed but not yet
/// pushed. Local edits live in <see cref="OutboxRow"/>, which also retains the ancestor they branched
/// from. Keeping the two apart is what makes a three-way merge possible at all — a single row that
/// held "current local state" would have overwritten the common ancestor and left only a two-way
/// diff, which cannot tell an edit from a revert.
/// </para>
/// <para>
/// <see cref="Payload"/> is the server's ciphertext byte for byte, so its AAD still verifies. Storing
/// a re-encrypted copy would work but would throw away the ability to detect that the server handed
/// back something it should not have.
/// </para>
/// </remarks>
internal sealed class CachedItemRow
{
    public Guid VaultId { get; set; }

    public SyncEntityType EntityType { get; set; }

    public Guid EntityId { get; set; }

    /// <summary>The server-assigned item version, and the value a push must expect.</summary>
    public int Version { get; set; }

    public long ChangeSequence { get; set; }

    public byte[]? Payload { get; set; }

    public byte[]? WrappedDataKey { get; set; }

    public Guid? DataKeyId { get; set; }

    public uint KeyGeneration { get; set; }

    public byte AadVersion { get; set; }

    /// <summary>
    /// The plaintext columns the server needs, sealed under the LocalCacheKey.
    /// </summary>
    /// <remarks>
    /// Sealed rather than stored as columns because the cache can do better than the server here for
    /// free. The server must hold a relay-enabled host's address in the clear — it has to resolve it
    /// — but this machine already holds the key that decrypts the payload, so nothing is gained by
    /// leaving the address readable in a file that ends up in backups. No query needs these yet; when
    /// one does, the field it needs gets its own column and this comment gets revisited.
    /// </remarks>
    public byte[]? ProtectedFields { get; set; }

    /// <summary>A tombstone. Deletes are never hard, or an offline client could not learn of them.</summary>
    public bool IsDeleted { get; set; }

    public DateTimeOffset UpdatedAtUtc { get; set; }
}

/// <summary>
/// A local change that the server has not yet accepted.
/// </summary>
/// <remarks>
/// <para>
/// At most one row per item, and it carries the ancestor it branched from. That ancestor is the
/// entire reason a conflict can be merged rather than arbitrated: with it, the client can tell which
/// side changed which field.
/// </para>
/// <para>
/// <see cref="Attempts"/> and <see cref="LastError"/> exist so a permanently rejected operation can be
/// parked and shown rather than retried forever. An operation the server calls
/// <c>Invalid</c> will never succeed on retry, and spinning on it would block every change queued
/// behind it.
/// </para>
/// </remarks>
internal sealed class OutboxRow
{
    /// <summary>Local, monotonic. Defines the order changes are pushed in.</summary>
    public long Sequence { get; set; }

    /// <summary>
    /// The server's idempotency key for this operation.
    /// </summary>
    /// <remarks>
    /// Re-minted whenever the payload changes — see <c>OutboxStore.QueueAsync</c>. Keeping the old id
    /// across an edit would let the server answer <c>Duplicate</c> for an operation whose contents
    /// have since changed, silently discarding the newer edit.
    /// </remarks>
    public Guid OperationId { get; set; }

    public Guid VaultId { get; set; }

    public SyncEntityType EntityType { get; set; }

    public Guid EntityId { get; set; }

    public SyncOperation Operation { get; set; }

    /// <summary>The version the client believes the server holds. Null means create.</summary>
    public int? ExpectedVersion { get; set; }

    public byte[]? Payload { get; set; }

    public byte[]? WrappedDataKey { get; set; }

    public Guid? DataKeyId { get; set; }

    public uint KeyGeneration { get; set; }

    public byte AadVersion { get; set; }

    public byte[]? ProtectedFields { get; set; }

    // ---- The ancestor this edit branched from ----
    // Kept verbatim, including the fields the AAD binds, because without the generation, the data
    // key id and the version, the ancestor cannot be decrypted and the merge has no base.

    public int? AncestorVersion { get; set; }

    public byte[]? AncestorPayload { get; set; }

    public byte[]? AncestorWrappedDataKey { get; set; }

    public Guid? AncestorDataKeyId { get; set; }

    public uint? AncestorKeyGeneration { get; set; }

    public byte? AncestorAadVersion { get; set; }

    public byte[]? AncestorProtectedFields { get; set; }

    public DateTimeOffset QueuedAtUtc { get; set; }

    public int Attempts { get; set; }

    public string? LastError { get; set; }

    /// <summary>Set when the server rejected this outright, so it stops being retried.</summary>
    public bool IsParked { get; set; }
}

/// <summary>Where a vault's pull has reached.</summary>
internal sealed class SyncStateRow
{
    public Guid VaultId { get; set; }

    /// <summary>
    /// The last cursor the server issued. Opaque and integrity-tagged: a client must never
    /// construct or edit one, which is why this is stored verbatim and never parsed.
    /// </summary>
    public string? Cursor { get; set; }

    public uint KeyGeneration { get; set; }

    public DateTimeOffset? LastPulledAtUtc { get; set; }

    public DateTimeOffset? LastPushedAtUtc { get; set; }

    /// <summary>
    /// Observed difference between the server's clock and this machine's, from the last pull.
    /// </summary>
    /// <remarks>
    /// Recorded rather than corrected. Local timestamps are display metadata, never a merge input —
    /// the merge uses versions and the retained ancestor — so a skewed clock must not be able to
    /// decide which edit wins.
    /// </remarks>
    public long ServerTimeSkewMs { get; set; }
}

/// <summary>Something the merge had to override, or an item that could not be processed.</summary>
internal sealed class ConflictRow
{
    public Guid Id { get; set; }

    public Guid VaultId { get; set; }

    public SyncEntityType EntityType { get; set; }

    public Guid EntityId { get; set; }

    public ConflictKind Kind { get; set; }

    /// <summary>The discarded values, sealed under the LocalCacheKey.</summary>
    /// <remarks>
    /// Sealed because this is the one place the cache deliberately holds decrypted vault content: the
    /// value a merge overrode. It has to be readable to be useful and it is exactly as sensitive as
    /// the item it came from.
    /// </remarks>
    public byte[] Detail { get; set; } = [];

    public DateTimeOffset DetectedAtUtc { get; set; }

    public bool Acknowledged { get; set; }
}
