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

namespace DodoSSH.Client.Domain;

/// <summary>A decoded SSH key payload, together with the schema version it was written at.</summary>
/// <param name="Key">The key.</param>
/// <param name="SchemaVersion">The version the writing client used.</param>
public sealed record SshKeySecretDocument(SshKeySecret Key, int SchemaVersion)
{
    /// <inheritdoc cref="HostSecretDocument.IsReadOnly" />
    /// <remarks>
    /// The consequence is sharper for a key than for a host. Re-encoding an item written by a newer client
    /// drops the fields this build has no concept of — and if one of those fields were, say, a certificate
    /// or a second key format, the item would still decrypt, still look complete, and no longer
    /// authenticate. Refusing to write is the only safe answer.
    /// </remarks>
    public bool IsReadOnly => SchemaVersion > SshKeySecretCodec.CurrentSchemaVersion;
}

/// <summary>
/// Encodes and decodes the plaintext inside an SSH key item's encrypted payload.
/// </summary>
/// <remarks>
/// Mirrors <see cref="HostSecretCodec"/>, for the same reasons and with the same guarantees: JSON so a field
/// can be added without a migration, deterministic property order so an unchanged key does not look like a
/// change to the sync engine, and a separate mutable document type so a decode failure cannot produce a
/// half-built key that looks valid downstream.
/// </remarks>
public static class SshKeySecretCodec
{
    /// <summary>The schema version this build writes.</summary>
    public const int CurrentSchemaVersion = 1;

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

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

        var document = new SshKeyPayloadDocument
        {
            SchemaVersion = CurrentSchemaVersion,
            Label = key.Label,
            PrivateKeyPem = key.PrivateKeyPem,
            Passphrase = key.Passphrase,
            PublicKey = key.PublicKey,
            Notes = key.Notes,
        };

        return JsonSerializer.SerializeToUtf8Bytes(
            document, SshKeyPayloadJsonContext.Default.SshKeyPayloadDocument);
    }

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

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

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

        var candidate = new SshKeySecret
        {
            Label = parsed.Label ?? string.Empty,
            PrivateKeyPem = parsed.PrivateKeyPem ?? string.Empty,
            Passphrase = parsed.Passphrase,
            PublicKey = parsed.PublicKey,
            Notes = parsed.Notes,
        };

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

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

    public string? Label { get; set; }

    public string? PrivateKeyPem { get; set; }

    public string? Passphrase { get; set; }

    public string? PublicKey { get; set; }

    public string? Notes { get; set; }
}

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