using System.Diagnostics.CodeAnalysis;

namespace DodoSSH.Client.Domain;

/// <summary>What was done to an item.</summary>
public enum ActivityOperation
{
    /// <summary>It was created.</summary>
    Created = 0,

    /// <summary>It was changed.</summary>
    Updated = 1,

    /// <summary>It was deleted.</summary>
    Deleted = 2,
}

/// <summary>
/// One create, edit or delete of a keychain item, decrypted.
/// </summary>
/// <remarks>
/// <para>
/// <b><see cref="ChangedFields"/> holds names and never values.</b> That is the rule the whole type is built
/// around, and it is the same one ADR 0006 imposes on the server's own <c>detail</c> column: an audit log
/// that recorded what a password used to be would be a plaintext credential store with a vault drawn around
/// it. "Password" is what somebody needs to see; the old password is what nobody does.
/// </para>
/// <para>
/// <b><see cref="ItemLabel"/> is a copy, taken at the time.</b> Deleting the item is one of the three things
/// this records, so a lookup would resolve to nothing in exactly the case the entry matters most. It is also
/// what makes a rename readable — an entry saying "renamed 'old-db'" is useful, and one saying "renamed
/// 'prod-db'" because that is what it is called now is not.
/// </para>
/// </remarks>
public sealed record ActivityLogSecret : IVaultSecret
{
    /// <summary>What kind of item this was about, as the sync contract names it.</summary>
    /// <remarks>
    /// Stored as the wire enum's name rather than its number, so an entry written by a build that knows a
    /// kind this one does not still reads as something — an unknown name is shown as itself, where an
    /// unknown number would have to be shown as a number.
    /// </remarks>
    public required string ItemKind { get; init; }

    /// <summary>The item, so an entry can be traced to what it was about.</summary>
    public required Guid ItemId { get; init; }

    /// <summary>What the item was called at the time.</summary>
    public required string ItemLabel { get; init; }

    /// <summary>What was done.</summary>
    public ActivityOperation Operation { get; init; }

    /// <summary>
    /// The names of the fields that changed, separated by <c>", "</c>. Never their values.
    /// </summary>
    /// <remarks>
    /// <para>
    /// One string rather than a collection, and the choice is about equality. A plain
    /// <see cref="IReadOnlyList{T}"/> on a record gets reference equality from the compiler-generated
    /// <c>Equals</c>, which is the trap <see cref="JumpChain"/> exists to avoid — and a second type of that
    /// shape is a lot of machinery for a value that is written once and only ever displayed. The separator is
    /// unambiguous because these are C# property names, which cannot contain one.
    /// </para>
    /// <para>
    /// Empty for a create and for a delete, where "which fields" has no meaning — every field arrived, or all
    /// of them went. Empty is also the honest answer when an update's before and after could not be compared,
    /// which is why nothing reading this may take empty to mean "nothing changed".
    /// </para>
    /// </remarks>
    public string ChangedFields { get; init; } = string.Empty;

    /// <summary>When it happened.</summary>
    public required DateTimeOffset At { get; init; }

    /// <summary>Which machine it was done from, as that machine calls itself.</summary>
    public required string DeviceName { get; init; }

    /// <summary>Which account in this organisation did it.</summary>
    public Guid ActorUserId { get; init; }

    /// <summary>What this entry is called, derived from what it records.</summary>
    /// <inheritdoc cref="KnownHostSecret.Label" path="/remarks" />
    public string Label => $"{Operation} {ItemLabel}";

    /// <summary>Whether this is storable, and why not if it is not.</summary>
    public bool TryValidate([NotNullWhen(false)] out string? reason)
    {
        if (string.IsNullOrWhiteSpace(ItemKind))
        {
            reason = "An activity log entry needs the kind of item it was about.";
            return false;
        }

        if (ItemId == Guid.Empty)
        {
            reason = "An activity log entry needs the item it was about.";
            return false;
        }

        if (string.IsNullOrWhiteSpace(DeviceName))
        {
            reason = "An activity log entry needs the machine it was done from.";
            return false;
        }

        // The label is deliberately not checked. An item somebody created and never named has an empty one,
        // and refusing to record that would mean the log's completeness depended on the user's tidiness.
        reason = null;
        return true;
    }
}
