using ZB.MOM.WW.Secrets.Abstractions;

namespace ZB.MOM.WW.Secrets.Replicator.AkkaDotNet.Protocol;

/// <summary>
/// The wire representation of one encrypted secret row.
/// </summary>
/// <remarks>
/// <para>
/// This is a deliberate hand-written mirror of <see cref="StoredSecret"/> rather than the type
/// itself, and the duplication is the point. It makes the trust boundary <b>structural</b>: the wire
/// contract is a flat set of primitives that a reader can check against "ciphertext only, never the
/// KEK" in one glance, and a field added to <see cref="StoredSecret"/> in future cannot start
/// crossing the network merely by existing — someone has to add it here on purpose.
/// </para>
/// <para>
/// All fields are primitives (<see cref="string"/>, <see cref="byte"/> arrays, <see cref="long"/>,
/// <see cref="bool"/>) so the DTO round-trips through any serializer, including Akka's default JSON
/// one if an application never registers <see cref="SecretReplicationSerializer"/>. Timestamps are
/// ISO-8601 <c>"O"</c> strings for the same reason the stores use them: last-writer-wins compares
/// them for equality, so the representation has to be exact rather than merely close.
/// </para>
/// </remarks>
public sealed record SecretRowDto
{
    /// <summary>The normalized secret name.</summary>
    public required string Name { get; init; }

    /// <summary>Optional human-readable description.</summary>
    public string? Description { get; init; }

    /// <summary>The <see cref="SecretContentType"/> enum name.</summary>
    public required string ContentType { get; init; }

    /// <summary>AES-256-GCM ciphertext of the secret plaintext.</summary>
    public required byte[] Ciphertext { get; init; }

    /// <summary>Nonce sealing <see cref="Ciphertext"/>.</summary>
    public required byte[] Nonce { get; init; }

    /// <summary>Authentication tag for <see cref="Ciphertext"/>.</summary>
    public required byte[] Tag { get; init; }

    /// <summary>The per-secret data key, wrapped by the KEK. The KEK itself never travels.</summary>
    public required byte[] WrappedDek { get; init; }

    /// <summary>Nonce used to wrap <see cref="WrappedDek"/>.</summary>
    public required byte[] WrapNonce { get; init; }

    /// <summary>Authentication tag for <see cref="WrappedDek"/>.</summary>
    public required byte[] WrapTag { get; init; }

    /// <summary>Identifier of the KEK that wrapped the DEK — an identifier, not key material.</summary>
    public required string KekId { get; init; }

    /// <summary>Monotonic revision.</summary>
    public required long Revision { get; init; }

    /// <summary>Whether the row is a tombstone.</summary>
    public bool IsDeleted { get; init; }

    /// <summary>ISO-8601 deletion timestamp, or <see langword="null"/>.</summary>
    public string? DeletedUtc { get; init; }

    /// <summary>ISO-8601 creation timestamp.</summary>
    public required string CreatedUtc { get; init; }

    /// <summary>ISO-8601 last-update timestamp — half of the last-writer-wins ordering key.</summary>
    public required string UpdatedUtc { get; init; }

    /// <summary>Principal that created the row, if known.</summary>
    public string? CreatedBy { get; init; }

    /// <summary>Principal that last updated the row, if known.</summary>
    public string? UpdatedBy { get; init; }

    /// <summary>Projects a stored row onto the wire contract.</summary>
    /// <param name="row">The encrypted row.</param>
    /// <returns>The wire representation.</returns>
    public static SecretRowDto FromStoredSecret(StoredSecret row)
    {
        ArgumentNullException.ThrowIfNull(row);

        return new SecretRowDto
        {
            Name = row.Name.Value,
            Description = row.Description,
            ContentType = row.ContentType.ToString(),
            Ciphertext = row.Ciphertext,
            Nonce = row.Nonce,
            Tag = row.Tag,
            WrappedDek = row.WrappedDek,
            WrapNonce = row.WrapNonce,
            WrapTag = row.WrapTag,
            KekId = row.KekId,
            Revision = row.Revision,
            IsDeleted = row.IsDeleted,
            DeletedUtc = row.DeletedUtc?.ToString("O"),
            CreatedUtc = row.CreatedUtc.ToString("O"),
            UpdatedUtc = row.UpdatedUtc.ToString("O"),
            CreatedBy = row.CreatedBy,
            UpdatedBy = row.UpdatedBy,
        };
    }

    /// <summary>Rebuilds the stored row from the wire contract.</summary>
    /// <returns>The encrypted row.</returns>
    /// <exception cref="ArgumentException">The name or content type is not valid.</exception>
    public StoredSecret ToStoredSecret() => new()
    {
        // SecretName re-validates on construction, so a malformed or path-traversing name from a
        // peer is rejected here rather than reaching the store.
        Name = new SecretName(Name),
        Description = Description,
        ContentType = ParseContentType(ContentType),
        // `required` on a byte[] means "present in the payload", NOT "non-null" — a peer sending an
        // explicit JSON null satisfies it and yields a null array, which would then blow up deep in
        // the store's parameter binding instead of being rejected at the boundary.
        Ciphertext = RequireBlob(Ciphertext, nameof(Ciphertext)),
        Nonce = RequireBlob(Nonce, nameof(Nonce)),
        Tag = RequireBlob(Tag, nameof(Tag)),
        WrappedDek = RequireBlob(WrappedDek, nameof(WrappedDek)),
        WrapNonce = RequireBlob(WrapNonce, nameof(WrapNonce)),
        WrapTag = RequireBlob(WrapTag, nameof(WrapTag)),
        KekId = KekId ?? throw new ArgumentException("Replicated secret row has a null KekId."),
        Revision = Revision,
        IsDeleted = IsDeleted,
        DeletedUtc = DeletedUtc is null ? null : ParseUtc(DeletedUtc),
        CreatedUtc = ParseUtc(CreatedUtc),
        UpdatedUtc = ParseUtc(UpdatedUtc),
        CreatedBy = CreatedBy,
        UpdatedBy = UpdatedBy,
    };

    private static DateTimeOffset ParseUtc(string value) => DateTimeOffset.Parse(
        value,
        System.Globalization.CultureInfo.InvariantCulture,
        System.Globalization.DateTimeStyles.RoundtripKind);

    private static byte[] RequireBlob(byte[]? value, string field) =>
        value ?? throw new ArgumentException($"Replicated secret row has a null {field}.", field);

    // Enum.Parse is wrong for untrusted input on two counts: a numeric string that overflows the
    // underlying type throws OverflowException (which the receiving actor's ArgumentException filter
    // would not catch, restarting it in a redelivery loop), and any in-range number is accepted even
    // if it names no member — persisting an undefined enum value that later readers assume is valid.
    private static SecretContentType ParseContentType(string value) =>
        Enum.TryParse(value, out SecretContentType parsed) && Enum.IsDefined(parsed)
            ? parsed
            : throw new ArgumentException(
                $"Replicated secret row has an unrecognized content type '{value}'.", nameof(value));
}

/// <summary>One manifest entry on the wire: enough to decide who is newer, with no ciphertext.</summary>
public sealed record SecretManifestEntryDto
{
    /// <summary>The normalized secret name.</summary>
    public required string Name { get; init; }

    /// <summary>Monotonic revision.</summary>
    public required long Revision { get; init; }

    /// <summary>ISO-8601 last-update timestamp.</summary>
    public required string UpdatedUtc { get; init; }

    /// <summary>Whether the row is a tombstone.</summary>
    public bool IsDeleted { get; init; }

    /// <summary>Projects a manifest entry onto the wire contract.</summary>
    /// <param name="entry">The manifest entry.</param>
    /// <returns>The wire representation.</returns>
    public static SecretManifestEntryDto FromEntry(SecretManifestEntry entry)
    {
        ArgumentNullException.ThrowIfNull(entry);

        return new SecretManifestEntryDto
        {
            Name = entry.Name.Value,
            Revision = entry.Revision,
            UpdatedUtc = entry.UpdatedUtc.ToString("O"),
            IsDeleted = entry.IsDeleted,
        };
    }

    /// <summary>Rebuilds the manifest entry from the wire contract.</summary>
    /// <returns>The manifest entry.</returns>
    public SecretManifestEntry ToEntry() => new()
    {
        Name = new SecretName(Name),
        Revision = Revision,
        UpdatedUtc = DateTimeOffset.Parse(
            UpdatedUtc,
            System.Globalization.CultureInfo.InvariantCulture,
            System.Globalization.DateTimeStyles.RoundtripKind),
        IsDeleted = IsDeleted,
    };
}

/// <summary>
/// A node broadcasting its full inventory so peers can work out, in one round trip, both what they
/// need from it and what it needs from them.
/// </summary>
/// <param name="Entries">The sender's manifest.</param>
public sealed record SecretManifestAnnounce(IReadOnlyList<SecretManifestEntryDto> Entries);

/// <summary>A peer asking for the encrypted rows behind names it found it was missing or behind on.</summary>
/// <param name="Names">The requested secret names.</param>
public sealed record SecretPullRequest(IReadOnlyList<string> Names);

/// <summary>
/// Encrypted rows delivered to a peer — the reply to a <see cref="SecretPullRequest"/>, the
/// unsolicited push half of an anti-entropy exchange, and the live broadcast of a fresh local write.
/// </summary>
/// <param name="Rows">The encrypted rows.</param>
public sealed record SecretRowsMessage(IReadOnlyList<SecretRowDto> Rows);
