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

namespace DodoSSH.Client.Domain;

/// <summary>A decoded activity 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 ActivityLogSecretDocument(ActivityLogSecret Entry, int SchemaVersion)
{
    /// <inheritdoc cref="ConnectionLogSecretDocument.IsReadOnly" />
    public bool IsReadOnly => SchemaVersion > ActivityLogSecretCodec.CurrentSchemaVersion;
}

/// <summary>
/// Encodes and decodes the plaintext inside an activity log entry's encrypted payload.
/// </summary>
/// <remarks>
/// <see cref="ActivityLogSecret.ItemKind"/> travels as its name and not its number, which is the one thing
/// here worth deciding on purpose: item kinds are an open set, so a build that has not heard of the fifth one
/// can still show "PortForward" where a number would leave it showing "9".
/// </remarks>
public static class ActivityLogSecretCodec
{
    /// <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(ActivityLogSecret entry)
    {
        ArgumentNullException.ThrowIfNull(entry);

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

        var document = new ActivityLogPayloadDocument
        {
            SchemaVersion = CurrentSchemaVersion,
            ItemKind = entry.ItemKind,
            ItemId = entry.ItemId,
            ItemLabel = entry.ItemLabel,
            Operation = (int)entry.Operation,
            ChangedFields = entry.ChangedFields.Length == 0 ? null : entry.ChangedFields,
            At = entry.At,
            DeviceName = entry.DeviceName,
            ActorUserId = entry.ActorUserId,
        };

        return JsonSerializer.SerializeToUtf8Bytes(
            document, ActivityLogPayloadJsonContext.Default.ActivityLogPayloadDocument);
    }

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

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

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

        var candidate = new ActivityLogSecret
        {
            ItemKind = parsed.ItemKind ?? string.Empty,
            ItemId = parsed.ItemId,
            ItemLabel = parsed.ItemLabel ?? string.Empty,
            Operation = Enum.IsDefined((ActivityOperation)parsed.Operation)
                ? (ActivityOperation)parsed.Operation
                : default,
            ChangedFields = parsed.ChangedFields ?? string.Empty,
            At = parsed.At,
            DeviceName = parsed.DeviceName ?? string.Empty,
            ActorUserId = parsed.ActorUserId,
        };

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

        document = new ActivityLogSecretDocument(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 ActivityLogPayloadDocument
{
    public int SchemaVersion { get; set; }

    public string? ItemKind { get; set; }

    public Guid ItemId { get; set; }

    public string? ItemLabel { get; set; }

    public int Operation { get; set; }

    /// <remarks>
    /// Written as null when empty rather than as <c>""</c>, so that a create and a delete — which have no
    /// changed fields by definition — omit the property entirely instead of carrying an empty one.
    /// </remarks>
    public string? ChangedFields { get; set; }

    public DateTimeOffset At { get; set; }

    public string? DeviceName { get; set; }

    public Guid ActorUserId { get; set; }
}

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