using System.Diagnostics.CodeAnalysis;
using System.Globalization;

namespace DodoSSH.Client.Domain;

/// <summary>How a connection ended.</summary>
public enum ConnectionOutcome
{
    /// <summary>The session ran and then ended — by the user, by the remote, or by the process closing.</summary>
    /// <remarks>
    /// One value for all three, deliberately. From an auditor's side "this person had a shell on that machine
    /// for eleven minutes" is the fact; which of the two ends hung up first is not something this client can
    /// establish reliably — a tab close and a remote hangup both arrive as the pump finishing — and a field
    /// that guessed would be worse than one that does not claim to know.
    /// </remarks>
    Closed = 0,

    /// <summary>The connection was attempted and did not open.</summary>
    Failed = 1,

    /// <summary>The host key was not the pinned one, so the client refused before authenticating.</summary>
    /// <remarks>
    /// Its own outcome rather than a kind of <see cref="Failed"/>, because it is the only one that means
    /// something about the <em>host</em> rather than about the network or the credentials. A run of these on
    /// one machine is the single most interesting thing a connection log can show.
    /// </remarks>
    Refused = 2,
}

/// <summary>What kind of session a log entry is about.</summary>
public enum ConnectionKind
{
    /// <summary>An interactive terminal.</summary>
    Terminal = 0,

    /// <summary>An SFTP session for moving files.</summary>
    /// <remarks>
    /// Recorded separately and not hidden. Opening the file browser is a second login as far as the remote's
    /// own <c>auth.log</c> is concerned, so a log of ours that quietly omitted it would disagree with the
    /// host's — and the person comparing the two would be right to trust the host.
    /// </remarks>
    Sftp = 1,
}

/// <summary>
/// One connection that was made, decrypted.
/// </summary>
/// <remarks>
/// <para>
/// <b>What is here, and what deliberately is not.</b> The host's label, its item id, the address as dialled,
/// when it started, how long it lasted, how it ended, and which user on which device did it. An audit log
/// with no actor is not an audit log — the whole reason these sync is that an administrator will read them
/// once teams land — so the actor is recorded and the SSH username is not. The two are different questions:
/// "who in this organisation opened a shell" is what an audit answers, and "which account they logged in as"
/// is a detail of the host that the host's own logs already have.
/// </para>
/// <para>
/// <b>Written once, at close.</b> Every field is known by then, so an entry never needs a second write —
/// which is what keeps a synced log from needing a merge, an outbox row per update, or any way to collide
/// with itself. A connection that is still running is not in here at all; it is shown from the workspace's
/// live state, which is the only place that knows.
/// </para>
/// </remarks>
public sealed record ConnectionLogSecret : IVaultSecret
{
    /// <summary>What the host was called at the time, or a plain address when nothing named it.</summary>
    /// <remarks>
    /// A copy rather than a lookup through <see cref="HostId"/>, and that is the point of it: the bookmark
    /// can be renamed or deleted, and a history that changed retroactively when somebody tidied their
    /// keychain would be a history nobody could rely on.
    /// </remarks>
    public required string HostLabel { get; init; }

    /// <summary>The address as dialled, <c>user@host:port</c> style, or whatever was typed.</summary>
    public required string Address { get; init; }

    /// <summary>The host item this was, or null when the connection did not come from one.</summary>
    public Guid? HostId { get; init; }

    /// <summary>Whether this was a terminal or a file-transfer session.</summary>
    public ConnectionKind Kind { get; init; }

    /// <summary>When it started.</summary>
    public required DateTimeOffset StartedAt { get; init; }

    /// <summary>How long it lasted.</summary>
    /// <remarks>
    /// A duration rather than an end time, because it is the thing anybody reads — and because the two clocks
    /// involved are the same one, so storing both would be storing a value and its own arithmetic.
    /// </remarks>
    public TimeSpan Duration { get; init; }

    /// <summary>How it ended.</summary>
    public ConnectionOutcome Outcome { get; init; }

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

    /// <summary>Which account in this organisation made 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 => string.Create(CultureInfo.InvariantCulture, $"{HostLabel} ({Address})");

    /// <summary>Whether this is storable, and why not if it is not.</summary>
    /// <remarks>
    /// A negative duration is refused rather than clamped. It can only come from a payload written elsewhere
    /// — nothing here can produce one — and a log that displayed "-3 hours" would leave a reader unable to
    /// tell a corrupt entry from a clock they should worry about.
    /// </remarks>
    public bool TryValidate([NotNullWhen(false)] out string? reason)
    {
        if (string.IsNullOrWhiteSpace(HostLabel))
        {
            reason = "A connection log entry needs the host it was about.";
            return false;
        }

        if (string.IsNullOrWhiteSpace(Address))
        {
            reason = "A connection log entry needs the address that was dialled.";
            return false;
        }

        if (string.IsNullOrWhiteSpace(DeviceName))
        {
            reason = "A connection log entry needs the machine it was made from.";
            return false;
        }

        if (Duration < TimeSpan.Zero)
        {
            reason = "A connection cannot have lasted a negative amount of time.";
            return false;
        }

        if (HostId == Guid.Empty)
        {
            reason = "A host reference cannot be an empty id; use no host instead.";
            return false;
        }

        reason = null;
        return true;
    }
}
