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

namespace DodoSSH.Client.Domain;

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

/// <summary>
/// Encodes and decodes the plaintext inside a bucket item's encrypted payload.
/// </summary>
/// <remarks>
/// Mirrors <see cref="CredentialSecretCodec"/>, for the same reasons and with the same guarantees.
/// </remarks>
public static class ObjectStoreSecretCodec
{
    /// <summary>The schema version this build writes.</summary>
    public const int CurrentSchemaVersion = 1;

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

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

        var document = new ObjectStorePayloadDocument
        {
            SchemaVersion = CurrentSchemaVersion,
            Label = store.Label,
            Bucket = store.Bucket,
            AccessKeyId = store.AccessKeyId,
            SecretAccessKey = store.SecretAccessKey,
            Region = store.Region,
            Endpoint = store.Endpoint,
            UsePathStyle = store.UsePathStyle,
            Notes = store.Notes,
        };

        return JsonSerializer.SerializeToUtf8Bytes(
            document, ObjectStorePayloadJsonContext.Default.ObjectStorePayloadDocument);
    }

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

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

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

        var candidate = new ObjectStoreSecret
        {
            Label = parsed.Label ?? string.Empty,
            Bucket = parsed.Bucket ?? string.Empty,
            AccessKeyId = parsed.AccessKeyId ?? string.Empty,
            SecretAccessKey = parsed.SecretAccessKey ?? string.Empty,
            Region = parsed.Region,
            Endpoint = parsed.Endpoint,
            UsePathStyle = parsed.UsePathStyle,
            Notes = parsed.Notes,
        };

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

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

    public string? Label { get; set; }

    public string? Bucket { get; set; }

    public string? AccessKeyId { get; set; }

    public string? SecretAccessKey { get; set; }

    public string? Region { get; set; }

    public string? Endpoint { get; set; }

    public bool UsePathStyle { get; set; }

    public string? Notes { get; set; }
}

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