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

namespace DodoSSH.Client.Domain;

/// <summary>A decoded group payload, together with the schema version it was written at.</summary>
/// <param name="Group">The group.</param>
/// <param name="SchemaVersion">The version the writing client used.</param>
public sealed record HostGroupSecretDocument(HostGroupSecret Group, int SchemaVersion)
{
    /// <inheritdoc cref="HostSecretDocument.IsReadOnly" />
    public bool IsReadOnly => SchemaVersion > HostGroupSecretCodec.CurrentSchemaVersion;
}

/// <summary>
/// Encodes and decodes the plaintext inside a group item's encrypted payload.
/// </summary>
/// <remarks>
/// Mirrors <see cref="KnownHostSecretCodec"/>, for the same reasons and with the same guarantees. One field
/// makes this look like ceremony around a string, and it is not: what the JSON envelope buys is a schema
/// version, which is what lets a later build add a field without every older client silently dropping it on
/// the next edit. See <see cref="HostSecretDocument.IsReadOnly"/>.
/// </remarks>
public static class HostGroupSecretCodec
{
    /// <summary>The schema version this build writes.</summary>
    public const int CurrentSchemaVersion = 1;

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

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

        var document = new HostGroupPayloadDocument
        {
            SchemaVersion = CurrentSchemaVersion,
            Label = group.Label,
        };

        return JsonSerializer.SerializeToUtf8Bytes(
            document, HostGroupPayloadJsonContext.Default.HostGroupPayloadDocument);
    }

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

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

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

        var candidate = new HostGroupSecret { Label = parsed.Label ?? string.Empty };

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

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

    public string? Label { get; set; }
}

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