using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace DodoSSH.Client.Domain;

/// <summary>A decoded connection log payload, together with the schema version it was written at.</summary>
/// <param name="Entry">The entry.</param>
/// <param name="SchemaVersion">The version the writing client used.</param>
public sealed record ConnectionLogSecretDocument(ConnectionLogSecret Entry, int SchemaVersion)
{
    /// <inheritdoc cref="HostSecretDocument.IsReadOnly" />
    /// <remarks>
    /// Answered for consistency and never acted on: nothing edits a log entry, so there is no re-encode that
    /// could drop a newer client's field. It stays because the reconciler asks every kind.
    /// </remarks>
    public bool IsReadOnly => SchemaVersion > ConnectionLogSecretCodec.CurrentSchemaVersion;
}

/// <summary>
/// Encodes and decodes the plaintext inside a connection log entry's encrypted payload.
/// </summary>
/// <remarks>
/// <para>
/// Mirrors <see cref="KnownHostSecretCodec"/>. The two enums are written as numbers rather than names,
/// unlike <see cref="ActivityLogSecret.ItemKind"/>: they are closed sets this codec owns, where the item kind
/// is an open one that a newer build may extend.
/// </para>
/// <para>
/// An unknown enum value decodes to the default rather than failing the whole entry. A log written by a
/// newer client that has learned a fourth outcome is still worth showing with its host, its times and its
/// actor intact — refusing it would lose the entry to save the one field nobody could have acted on anyway.
/// </para>
/// </remarks>
public static class ConnectionLogSecretCodec
{
    /// <summary>The schema version this build writes.</summary>
    public const int CurrentSchemaVersion = 1;

    /// <summary>Serialises an entry to the bytes that get sealed.</summary>
    /// <exception cref="ArgumentException">The entry is not valid for storage.</exception>
    public static byte[] Encode(ConnectionLogSecret entry)
    {
        ArgumentNullException.ThrowIfNull(entry);

        if (!entry.TryValidate(out var reason))
        {
            throw new ArgumentException(reason, nameof(entry));
        }

        var document = new ConnectionLogPayloadDocument
        {
            SchemaVersion = CurrentSchemaVersion,
            HostLabel = entry.HostLabel,
            Address = entry.Address,
            HostId = entry.HostId,
            Kind = (int)entry.Kind,
            StartedAt = entry.StartedAt,
            DurationMs = (long)entry.Duration.TotalMilliseconds,
            Outcome = (int)entry.Outcome,
            DeviceName = entry.DeviceName,
            ActorUserId = entry.ActorUserId,
        };

        return JsonSerializer.SerializeToUtf8Bytes(
            document, ConnectionLogPayloadJsonContext.Default.ConnectionLogPayloadDocument);
    }

    /// <summary>Parses a decrypted payload.</summary>
    /// <inheritdoc cref="HostSecretCodec.TryDecode" path="/remarks" />
    public static bool TryDecode(
        ReadOnlySpan<byte> payload,
        [NotNullWhen(true)] out ConnectionLogSecretDocument? document)
    {
        document = null;

        ConnectionLogPayloadDocument? parsed;
        try
        {
            parsed = JsonSerializer.Deserialize(
                payload, ConnectionLogPayloadJsonContext.Default.ConnectionLogPayloadDocument);
        }
        catch (JsonException)
        {
            return false;
        }

        if (parsed is null || parsed.SchemaVersion < 1)
        {
            return false;
        }

        var candidate = new ConnectionLogSecret
        {
            HostLabel = parsed.HostLabel ?? string.Empty,
            Address = parsed.Address ?? string.Empty,
            HostId = parsed.HostId,
            Kind = Enum.IsDefined((ConnectionKind)parsed.Kind) ? (ConnectionKind)parsed.Kind : default,
            StartedAt = parsed.StartedAt,
            Duration = TimeSpan.FromMilliseconds(parsed.DurationMs),
            Outcome = Enum.IsDefined((ConnectionOutcome)parsed.Outcome)
                ? (ConnectionOutcome)parsed.Outcome
                : default,
            DeviceName = parsed.DeviceName ?? string.Empty,
            ActorUserId = parsed.ActorUserId,
        };

        if (!candidate.TryValidate(out _))
        {
            return false;
        }

        document = new ConnectionLogSecretDocument(candidate, parsed.SchemaVersion);
        return true;
    }
}

/// <summary>The serialised shape. Mutable and nullable because it models untrusted input.</summary>
/// <inheritdoc cref="HostPayloadDocument" path="/remarks" />
internal sealed class ConnectionLogPayloadDocument
{
    public int SchemaVersion { get; set; }

    public string? HostLabel { get; set; }

    public string? Address { get; set; }

    public Guid? HostId { get; set; }

    public int Kind { get; set; }

    public DateTimeOffset StartedAt { get; set; }

    /// <remarks>
    /// Milliseconds as an integer rather than a <see cref="TimeSpan"/>, which <c>System.Text.Json</c> writes
    /// as <c>"00:11:03.4560000"</c> — a format whose parsing varies between platforms and whose precision
    /// invites a round-trip that is nearly but not exactly the value written.
    /// </remarks>
    public long DurationMs { get; set; }

    public int Outcome { get; set; }

    public string? DeviceName { get; set; }

    public Guid ActorUserId { get; set; }
}

[JsonSourceGenerationOptions(
    PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
    UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip)]
[JsonSerializable(typeof(ConnectionLogPayloadDocument))]
internal sealed partial class ConnectionLogPayloadJsonContext : JsonSerializerContext;
