using System.Diagnostics.CodeAnalysis;
namespace DodoSSH.Client.Domain;
///
/// A host as the user sees it: everything the server never gets to read.
///
///
///
/// The whole of this record lives inside the item's encrypted payload. In particular there is no
/// plaintext label anywhere in the system — access-control administration runs on the client, which
/// can decrypt names, so the server never needs a searchable title.
///
///
/// and are here and may additionally appear as
/// plaintext columns on the server, but only for a host the user has opted into the relay. That is
/// the one deliberate privacy concession in the design: the relay must resolve its target
/// server-side or it becomes an authenticated open TCP proxy into the operator's own network. The
/// copy in here is the authoritative one; the plaintext column is a derived duplicate the client
/// supplies only when relay is enabled. See ADR 0004.
///
///
/// Structural equality holds across every field, including the collections, which is what the merge
/// relies on to tell "unchanged" from "changed to the same thing" from "changed differently".
///
///
public sealed record HostSecret
{
/// The default SSH port, used when a host does not say otherwise.
public const int DefaultPort = 22;
/// Display name. The only name this host has anywhere.
public required string Label { get; init; }
/// Hostname or address to connect to.
public required string Hostname { get; init; }
/// TCP port.
public int Port { get; init; } = DefaultPort;
/// Login user, when the host pins one.
public string? Username { get; init; }
/// Free-text notes.
public string? Notes { get; init; }
///
/// The jump chain, nearest hop first, as host item ids.
///
///
/// Order is the meaning here, so this merges as a whole value rather than as a set: reordering a
/// chain changes which machine is reached through which, and a set union of two different chains
/// would produce a route neither user asked for.
///
public JumpChain JumpHostIds { get; init; } = JumpChain.Empty;
/// SSH directives, unique by name.
public HostOptions Options { get; init; } = HostOptions.Empty;
///
/// Whether this host may be dialled through the server relay.
///
///
///
/// Lives here, inside the encrypted payload, rather than only in the plaintext columns the server
/// keeps. It has to: it is the flag that decides whether and
/// are copied out into those columns, and a setting the merge cannot see is a
/// setting two clients can silently disagree about — one of them re-exposing an address the other
/// had just withdrawn.
///
///
/// The plaintext copy is derived from this, in one place, so the address can only ever leave the
/// payload as a consequence of the user turning this on. See ADR 0004.
///
///
public bool RelayEnabled { get; init; }
///
/// Checks the fields that must hold before this can be stored.
///
///
/// Separate from construction on purpose. A view model binds directly to these properties and
/// passes through empty and half-typed states on the way to a valid one; a constructor that threw
/// would make the editor unusable. The sync layer validates before sealing, and the codec
/// validates on decode, which are the two points where an invalid host would become durable.
///
public bool TryValidate([NotNullWhen(false)] out string? error)
{
if (string.IsNullOrWhiteSpace(Label))
{
error = "A host needs a name.";
return false;
}
if (string.IsNullOrWhiteSpace(Hostname))
{
error = "A host needs a hostname or address.";
return false;
}
if (Port is < 1 or > 65535)
{
error = $"Port must be between 1 and 65535, not {Port}.";
return false;
}
if (JumpHostIds.AsSpan().Contains(Guid.Empty))
{
error = "A jump chain cannot contain an empty host id.";
return false;
}
error = null;
return true;
}
}