using System.Diagnostics.CodeAnalysis;

namespace DodoSSH.Client.Domain;

/// <summary>
/// A username and password as the user sees it, decrypted.
/// </summary>
/// <remarks>
/// <para>
/// The same bargain <see cref="SshKeySecret"/> documents applies here and is worth not repeating in full:
/// the password is an ordinary managed string, it cannot be wiped, and a process dump taken while the vault
/// is unlocked contains it. What that buys is that it never reaches the disk or the server in a form either
/// can read.
/// </para>
/// <para>
/// <see cref="Username"/> is optional and overrides the host's when set, which is the reason this is a
/// separate item rather than two more fields on a host: one credential is very often the same account on
/// twenty machines, and duplicating it per host means rotating it in twenty places and missing one.
/// </para>
/// </remarks>
public sealed record CredentialSecret : IVaultSecret
{
    private readonly string? username;

    /// <summary>What the user calls this credential.</summary>
    public required string Label { get; init; }

    /// <summary>
    /// The password.
    /// </summary>
    /// <remarks>
    /// Required, and an empty one is not valid — see <see cref="TryValidate"/>. A credential with no password
    /// is not a credential, and storing one would produce an item that looks usable and fails at the
    /// handshake with an error about authentication rather than about the vault.
    /// </remarks>
    public required string Password { get; init; }

    /// <summary>
    /// The account this credential is for, when it is not the host's own username.
    /// </summary>
    /// <remarks>
    /// Empty is normalised to null, as <see cref="SshKeySecret.Passphrase"/> is and for the same kind of
    /// reason: blank and absent mean one thing here — "use the host's username" — and two spellings of one
    /// state would give two clients different payload bytes for an identical credential, and make
    /// <c>Username is not null</c> an unreliable answer to "does this override the host?".
    /// </remarks>
    public string? Username
    {
        get => username;
        init => username = string.IsNullOrEmpty(value) ? null : value;
    }

    /// <summary>Free text.</summary>
    public string? Notes { get; init; }

    /// <summary>Whether this is storable, and why not if it is not.</summary>
    public bool TryValidate([NotNullWhen(false)] out string? reason)
    {
        if (string.IsNullOrWhiteSpace(Label))
        {
            reason = "A credential needs a name.";
            return false;
        }

        if (Password.Length == 0)
        {
            // Length rather than IsNullOrWhiteSpace: a password of spaces is a password, and refusing it
            // would lock someone out of a host over a validation opinion.
            reason = "A credential needs a password.";
            return false;
        }

        reason = null;
        return true;
    }
}
