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

namespace DodoSSH.Client.Domain;

/// <summary>A decoded known-host payload, together with the schema version it was written at.</summary>
/// <param name="KnownHost">The pin.</param>
/// <param name="SchemaVersion">The version the writing client used.</param>
public sealed record KnownHostSecretDocument(KnownHostSecret KnownHost, int SchemaVersion)
{
    /// <inheritdoc cref="HostSecretDocument.IsReadOnly" />
    public bool IsReadOnly => SchemaVersion > KnownHostSecretCodec.CurrentSchemaVersion;
}

/// <summary>
/// Encodes and decodes the plaintext inside a known-host item's encrypted payload.
/// </summary>
/// <remarks>
/// Mirrors <see cref="CredentialSecretCodec"/>, 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 pin 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 pin that looks valid downstream.
/// <para>
/// The label is not in here, and that is not an omission — <see cref="KnownHostSecret.Label"/> is derived
/// from the three fields that are. Writing it would put a value on the wire that a reader could disagree
/// with.
/// </para>
/// </remarks>
public static class KnownHostSecretCodec
{
    /// <summary>The schema version this build writes.</summary>
    public const int CurrentSchemaVersion = 1;

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

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

        var document = new KnownHostPayloadDocument
        {
            SchemaVersion = CurrentSchemaVersion,
            Host = knownHost.Host,
            Port = knownHost.Port,
            Algorithm = knownHost.Algorithm,
            Fingerprint = knownHost.Fingerprint,
        };

        return JsonSerializer.SerializeToUtf8Bytes(
            document, KnownHostPayloadJsonContext.Default.KnownHostPayloadDocument);
    }

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

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

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

        var candidate = new KnownHostSecret
        {
            Host = parsed.Host ?? string.Empty,
            Port = parsed.Port,
            Algorithm = parsed.Algorithm ?? string.Empty,
            Fingerprint = parsed.Fingerprint ?? string.Empty,
        };

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

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

    public string? Host { get; set; }

    public int Port { get; set; }

    public string? Algorithm { get; set; }

    public string? Fingerprint { get; set; }
}

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