namespace DodoSSH.Domain;

/// <summary>
/// An SSH host.
/// </summary>
/// <remarks>
/// <para>
/// The only vault item type in M1. Everything sensitive — username, notes, jump chain, SSH
/// options — lives inside <see cref="Payload"/>. There is deliberately no plaintext label:
/// access-control administration happens in the client, which can decrypt names, so the server
/// never needs a searchable title.
/// </para>
/// <para>
/// <see cref="Hostname"/> and <see cref="Port"/> are the one deliberate plaintext concession, and
/// only when <see cref="RelayEnabled"/> is set. The relay must resolve its target server-side or
/// it becomes an authenticated open TCP proxy into the operator's own network. A database CHECK
/// constraint enforces the pairing so it cannot drift. See ADR 0004.
/// </para>
/// <para>
/// <b>There is no group column, and there used to be one.</b> A <c>group_id</c> was reserved here from the
/// first migration and never written by any shipped client; it was dropped when groups actually landed,
/// because membership belongs inside <see cref="Payload"/>. The test ADR 0004 sets for a plaintext
/// concession is that the server <em>cannot function</em> without the value, and nothing on the server reads
/// a group — the client decrypts its own vault to draw the sidebar. What the column would have handed over
/// is a clustering of the estate: which machines this user files together, for free, in the clear. See
/// <see cref="VaultHostGroup"/>.
/// </para>
/// </remarks>
public sealed class SshHost : IVaultItem
{
    /// <summary>Primary key. UUIDv7, generated by the client so items can be created offline.</summary>
    public Guid Id { get; set; }

    /// <summary>Owning vault.</summary>
    public Guid VaultId { get; set; }

    /// <summary>Owning vault.</summary>
    public Vault? Vault { get; set; }

    /// <summary>The encrypted item: a DSH1 envelope. Opaque to the server.</summary>
    public byte[] Payload { get; set; } = [];

    /// <summary>The item's data key, wrapped under the vault key. Opaque.</summary>
    public byte[]? DataKeyWrap { get; set; }

    /// <summary>
    /// Reserved for per-item content keys wrapped to individual users, which is what will make
    /// per-item access control cryptographic rather than server-enforced. Present from the first
    /// migration so that lands without a migration; see docs/crypto.md §3.
    /// </summary>
    public Guid? ContentKeyId { get; set; }

    /// <summary>Vault key generation this payload was encrypted under.</summary>
    public int KeyGeneration { get; set; }

    /// <summary>AAD rule version, enabling a lazy re-encrypt-on-write migration later.</summary>
    public short PayloadAadVersion { get; set; }

    /// <summary>Whether this host may be dialled through the server relay.</summary>
    public bool RelayEnabled { get; set; }

    /// <summary>Target hostname. Permitted only when <see cref="RelayEnabled"/> is set.</summary>
    public string? Hostname { get; set; }

    /// <summary>Target port. Permitted only when <see cref="RelayEnabled"/> is set.</summary>
    public int? Port { get; set; }

    /// <summary>
    /// Client-visible, monotonic item version. Used for optimistic concurrency on push, and
    /// deliberately distinct from the internal <c>xmin</c> guard, which is never exposed because
    /// it is not stable across VACUUM FREEZE.
    /// </summary>
    public int Version { get; set; }

    /// <summary>Latest change-log sequence touching this row, so a delta pull can join directly.</summary>
    public long ChangeSequence { get; set; }

    /// <summary>Creation timestamp.</summary>
    public DateTimeOffset CreatedAtUtc { get; set; }

    /// <summary>Last modification timestamp.</summary>
    public DateTimeOffset UpdatedAtUtc { get; set; }

    /// <summary>
    /// Soft-delete marker. Deletes are tombstones: a client that has been offline must be able to
    /// learn an item went away, and a vanished row is indistinguishable from one never seen.
    /// </summary>
    public DateTimeOffset? DeletedAtUtc { get; set; }

    /// <summary>Who created it.</summary>
    public Guid CreatedByUserId { get; set; }

    /// <summary>Who last modified it.</summary>
    public Guid UpdatedByUserId { get; set; }
}

/// <summary>
/// An SSH key pair, held as ciphertext.
/// </summary>
/// <remarks>
/// <para>
/// The private key, its passphrase, its label and its comment are all inside <see cref="Payload"/>. This
/// row is the reason the vault is worth having — a key that syncs between a user's machines and can later be
/// shared with a teammate — and it is also the row that would hurt most if the server could read it, so
/// there is deliberately not one plaintext column of substance.
/// </para>
/// <para>
/// Notably absent: the relay trio. A key is not something the server dials, so the plaintext concession
/// ADR 0004 makes for <see cref="SshHost"/> has no analogue here and no reason to exist. That is the whole
/// argument for a separate table rather than one wide item table: the columns a host needs are columns a key
/// must never have, and a shared table could only make them nullable and trust the code.
/// </para>
/// <para>
/// <see cref="PublicKeyFingerprint"/> is the single exception, and it stays null until something needs it.
/// The contract reserved the slot for showing which key a host is configured to use without decrypting
/// every key first; write it only when that feature lands, and never derive anything security-relevant from
/// it, because a fingerprint the server stores is a fingerprint the server chose.
/// </para>
/// </remarks>
public sealed class VaultSshKey : IVaultItem
{
    /// <summary>Primary key. UUIDv7, generated by the client so keys can be created offline.</summary>
    public Guid Id { get; set; }

    /// <summary>Owning vault.</summary>
    public Guid VaultId { get; set; }

    /// <summary>Owning vault.</summary>
    public Vault? Vault { get; set; }

    /// <summary>The encrypted key material: a DSH1 envelope. Opaque to the server.</summary>
    public byte[] Payload { get; set; } = [];

    /// <summary>The item's data key, wrapped under the vault key. Opaque.</summary>
    public byte[]? DataKeyWrap { get; set; }

    /// <summary>Reserved for per-item content keys wrapped to individual users; see docs/crypto.md §3.</summary>
    public Guid? ContentKeyId { get; set; }

    /// <summary>Vault key generation this payload was encrypted under.</summary>
    public int KeyGeneration { get; set; }

    /// <summary>AAD rule version, enabling a lazy re-encrypt-on-write migration later.</summary>
    public short PayloadAadVersion { get; set; }

    /// <summary>
    /// OpenSSH-style <c>SHA256:base64</c> fingerprint of the public half, when a client publishes it.
    /// </summary>
    /// <remarks>
    /// Plaintext, and therefore opt-in and currently unused. A public key fingerprint is not a secret, but
    /// it is an identifier that links a vault to a machine's <c>authorized_keys</c>, so it is stored only
    /// when a feature needs it rather than because it is harmless.
    /// </remarks>
    public string? PublicKeyFingerprint { get; set; }

    /// <summary>Client-visible, monotonic item version, used for <c>expectedVersion</c> checks.</summary>
    public int Version { get; set; }

    /// <summary>Latest change-log sequence touching this row, so a delta pull can join directly.</summary>
    public long ChangeSequence { get; set; }

    /// <summary>Creation timestamp.</summary>
    public DateTimeOffset CreatedAtUtc { get; set; }

    /// <summary>Last modification timestamp.</summary>
    public DateTimeOffset UpdatedAtUtc { get; set; }

    /// <summary>Soft-delete marker; a tombstone, so an offline client learns the key went away.</summary>
    public DateTimeOffset? DeletedAtUtc { get; set; }

    /// <summary>Who created it.</summary>
    public Guid CreatedByUserId { get; set; }

    /// <summary>Who last modified it.</summary>
    public Guid UpdatedByUserId { get; set; }
}

/// <summary>
/// A stored username and password, as ciphertext.
/// </summary>
/// <remarks>
/// <para>
/// Its own table for the same reason <see cref="VaultSshKey"/> is: the columns a host needs are columns a
/// credential must never have. There is no relay trio here, and unlike a key there is not even a fingerprint
/// — nothing about a password is safe to hold in the clear, not its length, not a hash, not a hint. So this
/// row is an opaque envelope and its bookkeeping, and that is the whole design.
/// </para>
/// <para>
/// <b>The server cannot enforce anything about the contents, and should not pretend to.</b> Whether a
/// credential has a username, whether its password is empty, whether it is still valid — all of that is
/// inside the payload and belongs to the client. The one thing this row asserts is that the ciphertext
/// belongs to a vault and carries a version, which is what the write path needs to order changes.
/// </para>
/// </remarks>
public sealed class VaultCredential : IVaultItem
{
    /// <summary>Primary key. UUIDv7, generated by the client so credentials can be created offline.</summary>
    public Guid Id { get; set; }

    /// <summary>Owning vault.</summary>
    public Guid VaultId { get; set; }

    /// <summary>Owning vault.</summary>
    public Vault? Vault { get; set; }

    /// <summary>The encrypted credential: a DSH1 envelope. Opaque to the server.</summary>
    public byte[] Payload { get; set; } = [];

    /// <summary>The item's data key, wrapped under the vault key. Opaque.</summary>
    public byte[]? DataKeyWrap { get; set; }

    /// <summary>Reserved for per-item content keys wrapped to individual users; see docs/crypto.md §3.</summary>
    public Guid? ContentKeyId { get; set; }

    /// <summary>Vault key generation this payload was encrypted under.</summary>
    public int KeyGeneration { get; set; }

    /// <summary>AAD rule version, enabling a lazy re-encrypt-on-write migration later.</summary>
    public short PayloadAadVersion { get; set; }

    /// <summary>Client-visible, monotonic item version, used for <c>expectedVersion</c> checks.</summary>
    public int Version { get; set; }

    /// <summary>Latest change-log sequence touching this row, so a delta pull can join directly.</summary>
    public long ChangeSequence { get; set; }

    /// <summary>Creation timestamp.</summary>
    public DateTimeOffset CreatedAtUtc { get; set; }

    /// <summary>Last modification timestamp.</summary>
    public DateTimeOffset UpdatedAtUtc { get; set; }

    /// <summary>Soft-delete marker; a tombstone, so an offline client learns the credential went away.</summary>
    public DateTimeOffset? DeletedAtUtc { get; set; }

    /// <summary>Who created it.</summary>
    public Guid CreatedByUserId { get; set; }

    /// <summary>Who last modified it.</summary>
    public Guid UpdatedByUserId { get; set; }
}

/// <summary>
/// One SSH host key a user has decided to trust, as ciphertext.
/// </summary>
/// <remarks>
/// <para>
/// The host, the port, the key algorithm and the fingerprint are all inside <see cref="Payload"/>, and this
/// is the item type where that is least obvious and most deliberate. None of those four values is
/// confidential on its own — an operator publishes the fingerprint so it can be checked, and a
/// relay-enabled host's address is already in a plaintext column next door. Together and across a user's
/// whole vault they are something else: which machines that person reaches, and when they first reached
/// them. The server has no use for any of it, so it gets none of it, and there is no column here to put it
/// in even by mistake.
/// </para>
/// <para>
/// <b>Why the server holds these at all, given it cannot read them.</b> Trust-on-first-use is only as good
/// as its memory. Kept locally, a pin dies with the machine and the user is asked to approve the same host
/// on every device — training them to approve without looking, which is the one habit that makes the
/// warning worthless. Kept here, trust follows the user, and because the payload is sealed the server
/// cannot drop a pin to force a fresh first-use decision without the client noticing the item is gone.
/// </para>
/// <para>
/// Otherwise this is <see cref="VaultCredential"/>'s shape exactly: an opaque envelope and the bookkeeping
/// the write path needs to order changes. Its own table for the same reason — the columns a host needs are
/// columns this must never have.
/// </para>
/// </remarks>
public sealed class VaultKnownHostKey : IVaultItem
{
    /// <summary>Primary key. UUIDv7, generated by the client so a pin can be recorded offline.</summary>
    public Guid Id { get; set; }

    /// <summary>Owning vault.</summary>
    public Guid VaultId { get; set; }

    /// <summary>Owning vault.</summary>
    public Vault? Vault { get; set; }

    /// <summary>The encrypted pin: a DSH1 envelope. Opaque to the server.</summary>
    public byte[] Payload { get; set; } = [];

    /// <summary>The item's data key, wrapped under the vault key. Opaque.</summary>
    public byte[]? DataKeyWrap { get; set; }

    /// <summary>Reserved for per-item content keys wrapped to individual users; see docs/crypto.md §3.</summary>
    public Guid? ContentKeyId { get; set; }

    /// <summary>Vault key generation this payload was encrypted under.</summary>
    public int KeyGeneration { get; set; }

    /// <summary>AAD rule version, enabling a lazy re-encrypt-on-write migration later.</summary>
    public short PayloadAadVersion { get; set; }

    /// <summary>Client-visible, monotonic item version, used for <c>expectedVersion</c> checks.</summary>
    public int Version { get; set; }

    /// <summary>Latest change-log sequence touching this row, so a delta pull can join directly.</summary>
    public long ChangeSequence { get; set; }

    /// <summary>Creation timestamp.</summary>
    public DateTimeOffset CreatedAtUtc { get; set; }

    /// <summary>Last modification timestamp.</summary>
    public DateTimeOffset UpdatedAtUtc { get; set; }

    /// <summary>
    /// Soft-delete marker; a tombstone, so an offline client learns the pin was withdrawn.
    /// </summary>
    /// <remarks>
    /// A tombstone matters more here than for the other types. Withdrawing trust is the deliberate act a
    /// user performs when a server has legitimately been rebuilt, and a pin that simply vanished from one
    /// machine's view would come back on the next pull — leaving the rebuilt host permanently unreachable
    /// from the machine that had the old key.
    /// </remarks>
    public DateTimeOffset? DeletedAtUtc { get; set; }

    /// <summary>Who created it.</summary>
    public Guid CreatedByUserId { get; set; }

    /// <summary>Who last modified it.</summary>
    public Guid UpdatedByUserId { get; set; }
}

/// <summary>
/// A folder hosts can be filed under, as ciphertext.
/// </summary>
/// <remarks>
/// <para>
/// Everything a group is — its name, the group it sits under, and the port, username and binding hosts
/// inside it fall back to — is inside the envelope, so this row is one of the narrowest in the schema: an
/// envelope and its bookkeeping. There is no <c>parent_id</c> and no <c>name</c> column, and both absences
/// are deliberate.
/// </para>
/// <para>
/// <b>No parent column, although groups do nest.</b> The pointer exists; it lives in the payload, and the
/// push path refuses a plaintext <c>ParentId</c> outright. What a column here would hand the operator is the
/// shape of every user's estate — how many groupings, how deep, which under which — which is the same
/// disclosure a plaintext <c>group_id</c> on a host would have been, and ADR 0004 spends the one plaintext
/// concession this design allows on the relay address instead.
/// </para>
/// <para>
/// The consequence is that the server cannot see a cycle, and it was once the argument for having no parent
/// at all: two offline clients can each re-parent A under B and B under A, a scalar merge accepts both, and
/// nothing server-side can refuse the pair. That is still true and is now handled rather than avoided —
/// every walk of the chain carries a visited set and stops at a repeat, so a cycle degrades to a group that
/// reads as a root. See <c>HostGroupSecret</c> on the client, which is the only thing that can read any of
/// this.
/// </para>
/// <para>
/// <b>No name, for the reason <see cref="VaultKnownHostKey"/> has no host column.</b> A group name is not
/// confidential the way a password is, but the set of names one person files their machines under is a
/// description of the estate — "customer-a", "pci", "on-call" — and the server has no use for any of it. It
/// sorts nothing; the client decrypts its own vault to draw a list.
/// </para>
/// <para>
/// <b>Membership lives on the host, not here.</b> The alternative — a member id list in this payload — would
/// make adding one host to a group a write to the group, so two clients adding two different hosts at once
/// would collide on one item. <c>ThreeWayMerge</c> has no set merge, so that collision would resolve by one
/// side winning and the other host quietly leaving the group. One pointer per host makes each of those two
/// operations a write to a different item, which cannot collide at all.
/// </para>
/// </remarks>
public sealed class VaultHostGroup : IVaultItem
{
    /// <summary>Primary key. UUIDv7, generated by the client so a group can be created offline.</summary>
    public Guid Id { get; set; }

    /// <summary>Owning vault.</summary>
    public Guid VaultId { get; set; }

    /// <summary>Owning vault.</summary>
    public Vault? Vault { get; set; }

    /// <summary>The encrypted group: a DSH1 envelope. Opaque to the server.</summary>
    public byte[] Payload { get; set; } = [];

    /// <summary>The item's data key, wrapped under the vault key. Opaque.</summary>
    public byte[]? DataKeyWrap { get; set; }

    /// <summary>Reserved for per-item content keys wrapped to individual users; see docs/crypto.md §3.</summary>
    public Guid? ContentKeyId { get; set; }

    /// <summary>Vault key generation this payload was encrypted under.</summary>
    public int KeyGeneration { get; set; }

    /// <summary>AAD rule version, enabling a lazy re-encrypt-on-write migration later.</summary>
    public short PayloadAadVersion { get; set; }

    /// <summary>Client-visible, monotonic item version, used for <c>expectedVersion</c> checks.</summary>
    public int Version { get; set; }

    /// <summary>Latest change-log sequence touching this row, so a delta pull can join directly.</summary>
    public long ChangeSequence { get; set; }

    /// <summary>Creation timestamp.</summary>
    public DateTimeOffset CreatedAtUtc { get; set; }

    /// <summary>Last modification timestamp.</summary>
    public DateTimeOffset UpdatedAtUtc { get; set; }

    /// <summary>
    /// Soft-delete marker; a tombstone, so an offline client learns the group went away.
    /// </summary>
    /// <remarks>
    /// Deleting a group leaves every host that named it holding an id that resolves to nothing, and that is
    /// the intended outcome rather than an oversight: those hosts fall back to the ungrouped heading. The
    /// alternative is rewriting N host payloads inside one delete, which turns a single user action into N
    /// pushes, N outbox rows and N chances to merge against a change nobody made.
    /// </remarks>
    public DateTimeOffset? DeletedAtUtc { get; set; }

    /// <summary>Who created it.</summary>
    public Guid CreatedByUserId { get; set; }

    /// <summary>Who last modified it.</summary>
    public Guid UpdatedByUserId { get; set; }
}

/// <summary>
/// A label that can be put on many hosts, as ciphertext.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="VaultHostGroup"/>'s shape exactly — an envelope and its bookkeeping — and the reasoning for the
/// missing <c>name</c> column is the same argument at higher stakes. A group name describes how one user
/// files their machines; a tag name describes what the machines <em>are</em>, and one tag spans twenty hosts
/// by design. A column here would give the operator a labelled map of every estate on the server, to sort a
/// list nothing server-side draws.
/// </para>
/// <para>
/// <b>No membership, and no join table either.</b> Which hosts wear this tag is a set of ids inside each
/// host's payload. <c>SyncEntityType.HostTag</c> reserves a slot for the join and it stays reserved: the one
/// thing a join buys over a set on the host — two people tagging the same host without one of them losing —
/// is already bought by the client's per-key three-way merge, and a second item type is a second table, a
/// second migration and a second pass through every rule in this file for nothing.
/// </para>
/// </remarks>
public sealed class VaultTag : IVaultItem
{
    /// <summary>Primary key. UUIDv7, generated by the client so a tag can be created offline.</summary>
    public Guid Id { get; set; }

    /// <summary>Owning vault.</summary>
    public Guid VaultId { get; set; }

    /// <summary>Owning vault.</summary>
    public Vault? Vault { get; set; }

    /// <summary>The encrypted tag: a DSH1 envelope. Opaque to the server.</summary>
    public byte[] Payload { get; set; } = [];

    /// <summary>The item's data key, wrapped under the vault key. Opaque.</summary>
    public byte[]? DataKeyWrap { get; set; }

    /// <summary>Reserved for per-item content keys wrapped to individual users; see docs/crypto.md §3.</summary>
    public Guid? ContentKeyId { get; set; }

    /// <summary>Vault key generation this payload was encrypted under.</summary>
    public int KeyGeneration { get; set; }

    /// <summary>AAD rule version, enabling a lazy re-encrypt-on-write migration later.</summary>
    public short PayloadAadVersion { get; set; }

    /// <summary>Client-visible, monotonic item version, used for <c>expectedVersion</c> checks.</summary>
    public int Version { get; set; }

    /// <summary>Latest change-log sequence touching this row, so a delta pull can join directly.</summary>
    public long ChangeSequence { get; set; }

    /// <summary>Creation timestamp.</summary>
    public DateTimeOffset CreatedAtUtc { get; set; }

    /// <summary>Last modification timestamp.</summary>
    public DateTimeOffset UpdatedAtUtc { get; set; }

    /// <summary>
    /// Soft-delete marker; a tombstone, so an offline client learns the tag went away.
    /// </summary>
    /// <remarks>
    /// Deleting a tag leaves every host wearing it holding an id that resolves to nothing, and that is the
    /// intended outcome: those hosts simply stop drawing that chip. Rewriting N host payloads inside one
    /// delete would turn a single user action into N pushes, and each of those is a read-modify-write of a
    /// set that can lose a merge — which would leave the vault holding a tag that is deleted and still worn.
    /// </remarks>
    public DateTimeOffset? DeletedAtUtc { get; set; }

    /// <summary>Who created it.</summary>
    public Guid CreatedByUserId { get; set; }

    /// <summary>Who last modified it.</summary>
    public Guid UpdatedByUserId { get; set; }
}

/// <summary>
/// An S3-compatible bucket and the credentials that reach it, as ciphertext.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="VaultCredential"/>'s shape, and it holds the same class of thing: a secret access key is a
/// password by another name, and everything around it — the endpoint, the region, the bucket — describes
/// somewhere the user keeps data. There is no plaintext column and there is no argument for one; the relay
/// does not dial a bucket, so ADR 0004's single concession has no analogue here.
/// </para>
/// <para>
/// <b>The endpoint is inside the payload even though it is often <c>s3.amazonaws.com</c>.</b> For everybody
/// self-hosting MinIO or Ceph it is an address on their own network, which is precisely the thing the host
/// table only stores in the clear when the relay cannot work without it. Nothing on the server dials this.
/// </para>
/// </remarks>
public sealed class VaultObjectStore : IVaultItem
{
    /// <summary>Primary key. UUIDv7, generated by the client so a bucket can be added offline.</summary>
    public Guid Id { get; set; }

    /// <summary>Owning vault.</summary>
    public Guid VaultId { get; set; }

    /// <summary>Owning vault.</summary>
    public Vault? Vault { get; set; }

    /// <summary>The encrypted bucket and its keys: a DSH1 envelope. Opaque to the server.</summary>
    public byte[] Payload { get; set; } = [];

    /// <summary>The item's data key, wrapped under the vault key. Opaque.</summary>
    public byte[]? DataKeyWrap { get; set; }

    /// <summary>Reserved for per-item content keys wrapped to individual users; see docs/crypto.md §3.</summary>
    public Guid? ContentKeyId { get; set; }

    /// <summary>Vault key generation this payload was encrypted under.</summary>
    public int KeyGeneration { get; set; }

    /// <summary>AAD rule version, enabling a lazy re-encrypt-on-write migration later.</summary>
    public short PayloadAadVersion { get; set; }

    /// <summary>Client-visible, monotonic item version, used for <c>expectedVersion</c> checks.</summary>
    public int Version { get; set; }

    /// <summary>Latest change-log sequence touching this row, so a delta pull can join directly.</summary>
    public long ChangeSequence { get; set; }

    /// <summary>Creation timestamp.</summary>
    public DateTimeOffset CreatedAtUtc { get; set; }

    /// <summary>Last modification timestamp.</summary>
    public DateTimeOffset UpdatedAtUtc { get; set; }

    /// <summary>Soft-delete marker; a tombstone, so an offline client learns the bucket went away.</summary>
    public DateTimeOffset? DeletedAtUtc { get; set; }

    /// <summary>Who created it.</summary>
    public Guid CreatedByUserId { get; set; }

    /// <summary>Who last modified it.</summary>
    public Guid UpdatedByUserId { get; set; }
}

/// <summary>
/// One connection that was made, as ciphertext.
/// </summary>
/// <remarks>
/// <para>
/// <b>Written once, at close, and never updated.</b> That single decision is what makes a synced log
/// tractable: an entry that is created and then left alone needs no three-way merge, produces exactly one
/// outbox row, and cannot collide with the unique index that allows one outbox row per item. A connection
/// that is still open is shown from the workspace's own in-memory state instead, because that is where the
/// truth about a running shell actually lives.
/// </para>
/// <para>
/// <b>Synced, and that is a deliberate trade rather than the obvious choice.</b> A log kept on the machine
/// that produced it cannot be read by an administrator, cannot survive a reinstall, and cannot be checked
/// against anything. Keeping it here buys team auditing; what it costs is that the operator learns a user's
/// connection <em>rate and timing</em> from row counts and <c>updated_at</c>, even though every field inside
/// is sealed. ADR 0001 already concedes it cannot hide that class of metadata; this widens it, and says so.
/// </para>
/// <para>
/// The shape is <see cref="VaultCredential"/>'s: an envelope and its bookkeeping, no plaintext column of any
/// kind. Which machines somebody reaches and when is the single most revealing thing this schema could hold
/// in the clear, which is exactly why it holds none of it.
/// </para>
/// </remarks>
public sealed class VaultConnectionLogEntry : IVaultItem
{
    /// <summary>Primary key. UUIDv7, generated by the client, so an entry can be written offline.</summary>
    public Guid Id { get; set; }

    /// <summary>Owning vault.</summary>
    public Guid VaultId { get; set; }

    /// <summary>Owning vault.</summary>
    public Vault? Vault { get; set; }

    /// <summary>The encrypted entry: a DSH1 envelope. Opaque to the server.</summary>
    public byte[] Payload { get; set; } = [];

    /// <summary>The item's data key, wrapped under the vault key. Opaque.</summary>
    public byte[]? DataKeyWrap { get; set; }

    /// <summary>Reserved for per-item content keys wrapped to individual users; see docs/crypto.md §3.</summary>
    public Guid? ContentKeyId { get; set; }

    /// <summary>Vault key generation this payload was encrypted under.</summary>
    public int KeyGeneration { get; set; }

    /// <summary>AAD rule version, enabling a lazy re-encrypt-on-write migration later.</summary>
    public short PayloadAadVersion { get; set; }

    /// <summary>
    /// Client-visible, monotonic item version.
    /// </summary>
    /// <remarks>
    /// Always 1 in practice, because nothing updates a log entry. The column stays because the shared write
    /// path needs it, and a kind that opted out of the version check would be a second write path.
    /// </remarks>
    public int Version { get; set; }

    /// <summary>Latest change-log sequence touching this row, so a delta pull can join directly.</summary>
    public long ChangeSequence { get; set; }

    /// <summary>Creation timestamp.</summary>
    public DateTimeOffset CreatedAtUtc { get; set; }

    /// <summary>Last modification timestamp.</summary>
    public DateTimeOffset UpdatedAtUtc { get; set; }

    /// <summary>
    /// Soft-delete marker; a tombstone, so an offline client learns the entry was pruned.
    /// </summary>
    /// <remarks>
    /// Retention is the only thing that deletes one of these, and it is a real tombstone that pushes — which
    /// is why it is rate-limited rather than run on a timer. A prune that did not sync would delete the same
    /// thousand entries again on every machine, for ever.
    /// </remarks>
    public DateTimeOffset? DeletedAtUtc { get; set; }

    /// <summary>Who created it.</summary>
    public Guid CreatedByUserId { get; set; }

    /// <summary>Who last modified it.</summary>
    public Guid UpdatedByUserId { get; set; }
}

/// <summary>
/// One create, edit or delete of a vault item, as ciphertext.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="VaultConnectionLogEntry"/>'s shape and its write-once rule, for a different history: what
/// changed in the keychain rather than what was connected to. The payload records the <em>names</em> of the
/// fields that changed and never their values, which is the same rule ADR 0006 imposes on the server's own
/// <c>detail</c> column. A log that recorded an old password would be a plaintext credential store with no
/// vault around it.
/// </para>
/// <para>
/// Note the deliberate asymmetry with the outbox, because it reads as a discrepancy otherwise: the outbox
/// <em>coalesces</em> two edits of one item into a single pending row, and this log does not — two edits are
/// two lines. The outbox describes what still has to be sent; this describes what somebody did.
/// </para>
/// </remarks>
public sealed class VaultActivityLogEntry : IVaultItem
{
    /// <summary>Primary key. UUIDv7, generated by the client, so an entry can be written offline.</summary>
    public Guid Id { get; set; }

    /// <summary>Owning vault.</summary>
    public Guid VaultId { get; set; }

    /// <summary>Owning vault.</summary>
    public Vault? Vault { get; set; }

    /// <summary>The encrypted entry: a DSH1 envelope. Opaque to the server.</summary>
    public byte[] Payload { get; set; } = [];

    /// <summary>The item's data key, wrapped under the vault key. Opaque.</summary>
    public byte[]? DataKeyWrap { get; set; }

    /// <summary>Reserved for per-item content keys wrapped to individual users; see docs/crypto.md §3.</summary>
    public Guid? ContentKeyId { get; set; }

    /// <summary>Vault key generation this payload was encrypted under.</summary>
    public int KeyGeneration { get; set; }

    /// <summary>AAD rule version, enabling a lazy re-encrypt-on-write migration later.</summary>
    public short PayloadAadVersion { get; set; }

    /// <inheritdoc cref="VaultConnectionLogEntry.Version" />
    public int Version { get; set; }

    /// <summary>Latest change-log sequence touching this row, so a delta pull can join directly.</summary>
    public long ChangeSequence { get; set; }

    /// <summary>Creation timestamp.</summary>
    public DateTimeOffset CreatedAtUtc { get; set; }

    /// <summary>Last modification timestamp.</summary>
    public DateTimeOffset UpdatedAtUtc { get; set; }

    /// <inheritdoc cref="VaultConnectionLogEntry.DeletedAtUtc" />
    public DateTimeOffset? DeletedAtUtc { get; set; }

    /// <summary>Who created it.</summary>
    public Guid CreatedByUserId { get; set; }

    /// <summary>Who last modified it.</summary>
    public Guid UpdatedByUserId { get; set; }
}

/// <summary>
/// A saved command, as ciphertext.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="VaultCredential"/>'s shape exactly, and for a stronger reason than symmetry. A snippet is a
/// command somebody runs on their infrastructure — <c>systemctl restart</c> against a named unit, a
/// <c>psql</c> line naming a database — and read as a set it describes what the estate is made of at least
/// as precisely as a list of hostnames does. There is nothing here the server could sort by that is worth
/// what holding it would cost, so there is no plaintext column to put it in even by mistake.
/// </para>
/// <para>
/// Whether the snippet runs on its own or merely gets typed into the terminal is inside the payload too, and
/// that is worth stating: it is the field the whole feature's safety rests on, so it must be one that
/// travels sealed and merges with everything else it belongs to, rather than a flag the server could see or
/// change.
/// </para>
/// </remarks>
public sealed class VaultSnippet : IVaultItem
{
    /// <summary>Primary key. UUIDv7, generated by the client so a snippet can be written offline.</summary>
    public Guid Id { get; set; }

    /// <summary>Owning vault.</summary>
    public Guid VaultId { get; set; }

    /// <summary>Owning vault.</summary>
    public Vault? Vault { get; set; }

    /// <summary>The encrypted snippet: a DSH1 envelope. Opaque to the server.</summary>
    public byte[] Payload { get; set; } = [];

    /// <summary>The item's data key, wrapped under the vault key. Opaque.</summary>
    public byte[]? DataKeyWrap { get; set; }

    /// <summary>Reserved for per-item content keys wrapped to individual users; see docs/crypto.md §3.</summary>
    public Guid? ContentKeyId { get; set; }

    /// <summary>Vault key generation this payload was encrypted under.</summary>
    public int KeyGeneration { get; set; }

    /// <summary>AAD rule version, enabling a lazy re-encrypt-on-write migration later.</summary>
    public short PayloadAadVersion { get; set; }

    /// <summary>Client-visible, monotonic item version, used for <c>expectedVersion</c> checks.</summary>
    public int Version { get; set; }

    /// <summary>Latest change-log sequence touching this row, so a delta pull can join directly.</summary>
    public long ChangeSequence { get; set; }

    /// <summary>Creation timestamp.</summary>
    public DateTimeOffset CreatedAtUtc { get; set; }

    /// <summary>Last modification timestamp.</summary>
    public DateTimeOffset UpdatedAtUtc { get; set; }

    /// <summary>Soft-delete marker; a tombstone, so an offline client learns the snippet went away.</summary>
    public DateTimeOffset? DeletedAtUtc { get; set; }

    /// <summary>Who created it.</summary>
    public Guid CreatedByUserId { get; set; }

    /// <summary>Who last modified it.</summary>
    public Guid UpdatedByUserId { get; set; }
}
