using System.Text;
using Nostr.Client.Messages;

namespace NostrRelay;

/// <summary>
/// Simple fixed length nostr event storage
/// </summary>
public static class NostrBuf
{
    public static byte[] Encode(NostrEvent ev)
    {
        var buf = new byte[CalculateSize(ev)];
        var span = buf.AsSpan();
        span[0] = 0x01; // version 1
        Convert.FromHexString(ev.Id!).CopyTo(span[1..33]);
        Convert.FromHexString(ev.Pubkey!).CopyTo(span[33..65]);
        Convert.FromHexString(ev.Sig!).CopyTo(span[65..129]);
        BitConverter.GetBytes((ulong)ToUnixTime(ev.CreatedAt!.Value)).CopyTo(span[129..137]);
        BitConverter.GetBytes((uint)ev.Kind).CopyTo(span[137..141]);
        var contentBytes = Encoding.UTF8.GetBytes(ev.Content!);
        BitConverter.GetBytes((uint)contentBytes.Length).CopyTo(span[141..145]);
        var pos = 145;
        contentBytes.CopyTo(span[pos..(pos += contentBytes.Length)]);
        BitConverter.GetBytes((ushort)ev.Tags!.Count).CopyTo(span[pos..(pos += 2)]);

        foreach (var tag in ev.Tags)
        {
            var tagKey = Encoding.UTF8.GetBytes(tag.TagIdentifier);
            span[pos++] = (byte)tagKey.Length;
            tagKey.CopyTo(span[pos..(pos += tagKey.Length)]);

            span[pos++] = (byte)tag.AdditionalData.Length;
            foreach (var tagAdd in tag.AdditionalData)
            {
                var tagAddBytes = Encoding.UTF8.GetBytes(tagAdd);
                BitConverter.GetBytes((ushort)tagAddBytes.Length).CopyTo(span[pos..(pos += 2)]);
                tagAddBytes.CopyTo(span[pos..(pos += tagAddBytes.Length)]);
            }
        }

        return buf;
    }

    public static NostrEvent? Decode(Span<byte> data)
    {
        var version = data[0];
        if (version != 0x01) throw new Exception("Version not supported");

        var id = Convert.ToHexString(data[1..33]).ToLower();
        var pubkey = Convert.ToHexString(data[33..65]).ToLower();
        var sig = Convert.ToHexString(data[65..129]).ToLower();
        var createdAt = BitConverter.ToUInt64(data[129..137]);
        var kind = BitConverter.ToUInt32(data[137..141]);
        var contentLen = BitConverter.ToUInt32(data[141..145]);
        var pos = 145;
        var content = Encoding.UTF8.GetString(data[pos..(pos += (int)contentLen)]);
        var nTags = BitConverter.ToUInt16(data[pos..(pos += 2)]);

        var tags = new List<NostrEventTag>();
        for (var x = 0; x < nTags; x++)
        {
            var keyLen = data[pos++];
            var keyString = Encoding.UTF8.GetString(data[pos..(pos += keyLen)]);

            var nElements = data[pos++];
            var elms = new string[nElements];
            for (var y = 0; y < nElements; y++)
            {
                var elmLen = BitConverter.ToUInt16(data[pos..(pos += 2)]);
                var elmString = Encoding.UTF8.GetString(data[pos..(pos += elmLen)]);
                elms[y] = elmString;
            }

            tags.Add(new(keyString, elms));
        }

        return new NostrEvent()
        {
            Id = id,
            Pubkey = pubkey,
            Sig = sig,
            CreatedAt = DateTimeOffset.FromUnixTimeSeconds((long)createdAt).UtcDateTime,
            Kind = (NostrKind)kind,
            Content = content,
            Tags = new NostrEventTags(tags)
        };
    }

    public static long CalculateSize(NostrEvent ev)
    {
        const long fixedPart =
            1 // version byte
            + 32 // id
            + 32 // pubkey
            + 64 // sig
            + 8 // created_at (uint64)      
            + 4 // kind (uint32)
            + 4 // len(content) (uint32)
            + 2; // len(tags) (uint16)

        var variableLength =
            Encoding.UTF8.GetByteCount(ev.Content!)
            + ev.Tags!.Sum(a =>
                1 + // 1 byte for length of first tag element (key)
                Encoding.UTF8.GetByteCount(a.TagIdentifier) + // length of the tag key
                a.AdditionalData.Length + // 1 byte - number of elements
                a.AdditionalData.Sum(b => Encoding.UTF8.GetByteCount(b)) + // length of the content of the items in the tag
                (a.AdditionalData.Length * 2)); // 2 bytes per tag element for length prefix

        return fixedPart + variableLength;
    }

    private static long ToUnixTime(DateTime dt)
    {
        return dt.ToUniversalTime().Ticks / 10000000L - 62135596800L;
    }
}
