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

namespace DodoSSH.Client.Domain;

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

/// <summary>
/// Encodes and decodes the plaintext inside a credential item's encrypted payload.
/// </summary>
/// <remarks>
/// Mirrors <see cref="SshKeySecretCodec"/>, 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 credential 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 credential that looks valid downstream.
/// </remarks>
public static class CredentialSecretCodec
{
    /// <summary>The schema version this build writes.</summary>
    public const int CurrentSchemaVersion = 1;

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

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

        var document = new CredentialPayloadDocument
        {
            SchemaVersion = CurrentSchemaVersion,
            Label = credential.Label,
            Password = credential.Password,
            Username = credential.Username,
            Notes = credential.Notes,
        };

        return JsonSerializer.SerializeToUtf8Bytes(
            document, CredentialPayloadJsonContext.Default.CredentialPayloadDocument);
    }

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

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

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

        var candidate = new CredentialSecret
        {
            Label = parsed.Label ?? string.Empty,
            Password = parsed.Password ?? string.Empty,
            Username = parsed.Username,
            Notes = parsed.Notes,
        };

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

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

    public string? Label { get; set; }

    public string? Password { get; set; }

    public string? Username { get; set; }

    public string? Notes { get; set; }
}

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