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

namespace DodoSSH.Client.Domain;

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

/// <summary>
/// Encodes and decodes the plaintext inside a snippet item's encrypted payload.
/// </summary>
/// <remarks>
/// Mirrors <see cref="CredentialSecretCodec"/>. The one thing to be careful about here is
/// <see cref="SnippetSecret.RunsOnInsert"/>: it is a <see cref="bool"/>, so a payload that omits it decodes
/// as <see langword="false"/> — which is the safe direction, and deliberately the one a malformed or
/// truncated write falls in.
/// </remarks>
public static class SnippetSecretCodec
{
    /// <summary>The schema version this build writes.</summary>
    public const int CurrentSchemaVersion = 1;

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

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

        var document = new SnippetPayloadDocument
        {
            SchemaVersion = CurrentSchemaVersion,
            Label = snippet.Label,
            Command = snippet.Command,
            Notes = snippet.Notes,
            RunsOnInsert = snippet.RunsOnInsert,
        };

        return JsonSerializer.SerializeToUtf8Bytes(
            document, SnippetPayloadJsonContext.Default.SnippetPayloadDocument);
    }

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

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

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

        var candidate = new SnippetSecret
        {
            Label = parsed.Label ?? string.Empty,
            Command = parsed.Command ?? string.Empty,
            Notes = parsed.Notes,
            RunsOnInsert = parsed.RunsOnInsert,
        };

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

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

    public string? Label { get; set; }

    public string? Command { get; set; }

    public string? Notes { get; set; }

    /// <remarks>
    /// Not nullable, so its absence is <see langword="false"/> rather than a third state. The field decides
    /// whether inserting a snippet also presses Enter, and "we could not tell" has to resolve to the answer
    /// that does nothing.
    /// </remarks>
    public bool RunsOnInsert { get; set; }
}

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