using EmbedIO;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using nksrv.LobbyServer;
using Sodium;
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO.Compression;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;

namespace nksrv.Utils
{
    public class PacketDecryption
    {
        public static async Task<PacketDecryptResponse> DecryptOrReturnContentAsync(IHttpContext ctx)
        {
            byte[] bin = [];

            using MemoryStream buffer = new();

            var stream = ctx.Request.InputStream;

            var encoding = ctx.Request.Headers[HttpHeaderNames.ContentEncoding]?.Trim();

            Stream decryptedStream;
            switch (encoding)
            {
                case CompressionMethodNames.Gzip:
                    decryptedStream = new GZipStream(stream, CompressionMode.Decompress);
                    break;
                case CompressionMethodNames.Deflate:
                    decryptedStream = new DeflateStream(stream, CompressionMode.Decompress);
                    break;
                case CompressionMethodNames.None:
                case null:
                    decryptedStream = stream;
                    break;
                case "gzip,enc":
                    var responseLen = CBorReadItem(stream); // length of header (not including encrypted data)
                    stream.ReadByte(); // ignore padding
                    stream.ReadByte(); // ignore padding

                    var decryptionToken = CBorReadString(stream);
                    var nonce = CBorReadByteString(stream);

                    MemoryStream encryptedBytes = new();
                    stream.CopyTo(encryptedBytes);

                    var bytes = encryptedBytes.ToArray();

                    var key = LobbyHandler.GetInfo(decryptionToken) ?? throw HttpException.BadRequest("Invalid decryption token");
                    var additionalData = GenerateAdditionalData(decryptionToken, false);

                    var x = SecretAeadXChaCha20Poly1305.Decrypt(bytes, nonce, key.Keys.ReadSharedSecret, [.. additionalData]);

                    var ms = new MemoryStream(x);

                    var unkVal1 = ms.ReadByte();
                    var unkVal2 = ms.ReadByte();
                    var sequenceNumber = ReadCborInteger(ms);


                    var startPos = (int)ms.Position;

                    var contents = x.Skip(startPos).ToArray();
                    if (contents.Length != 0 && contents[0] == 31)
                    {
                        //File.WriteAllBytes("contentsgzip", contents);
                        // gzip compression is used
                        using Stream csStream = new GZipStream(new MemoryStream(contents), CompressionMode.Decompress);
                        using MemoryStream decoded = new();
                        csStream.CopyTo(decoded);

                        contents = decoded.ToArray();
                    }

                    return new PacketDecryptResponse() { Contents = contents, UserId = key.UserId, UsedAuthToken = decryptionToken };
                default:
                    throw HttpException.BadRequest($"Unsupported content encoding \"{encoding}\"");
            }


            await stream.CopyToAsync(buffer, 81920, ctx.CancellationToken).ConfigureAwait(continueOnCapturedContext: false);
            return new PacketDecryptResponse() { Contents = buffer.ToArray() };
        }

        public static ulong ReadCborInteger(Stream stream)
        {
            // Read the initial byte
            int initialByte = stream.ReadByte();
            if (initialByte == -1)
            {
                throw new EndOfStreamException("Stream ended unexpectedly");
            }

            // Major type is the first 3 bits of the initial byte
            int majorType = (initialByte >> 5) & 0x07;
            // Additional info is the last 5 bits of the initial byte
            int additionalInfo = initialByte & 0x1F;

            if (majorType != 0)
            {
                //throw new InvalidDataException("Not a valid CBOR unsigned integer");
            }

            ulong value;
            if (additionalInfo < 24)
            {
                value = (ulong)additionalInfo;
            }
            else if (additionalInfo == 24)
            {
                value = (ulong)stream.ReadByte();
            }
            else if (additionalInfo == 25)
            {
                Span<byte> buffer = stackalloc byte[2];
                if (stream.Read(buffer) != 2)
                {
                    throw new EndOfStreamException("Stream ended unexpectedly");
                }
                value = BinaryPrimitives.ReadUInt16BigEndian(buffer);
            }
            else if (additionalInfo == 26)
            {
                Span<byte> buffer = stackalloc byte[4];
                if (stream.Read(buffer) != 4)
                {
                    throw new EndOfStreamException("Stream ended unexpectedly");
                }
                value = BinaryPrimitives.ReadUInt32BigEndian(buffer);
            }
            else if (additionalInfo == 27)
            {
                Span<byte> buffer = stackalloc byte[8];
                if (stream.Read(buffer) != 8)
                {
                    throw new EndOfStreamException("Stream ended unexpectedly");
                }
                value = BinaryPrimitives.ReadUInt64BigEndian(buffer);
            }
            else
            {
                throw new InvalidDataException("Invalid additional info for CBOR unsigned integer");
            }

            return value;
        }

        public static byte[] EncryptData(byte[] message, string authToken)
        {
            var key = LobbyHandler.GetInfo(authToken) ?? throw HttpException.BadRequest("Invalid decryption token");
            MemoryStream m = new();

            m.WriteByte(89); // cbor ushort

            // 42bytes of data past header, 3 bytes for auth token bytestring, 2 bytes for nonce prefix, 24 bytes for nonce data
            var headerLen = 2 + 3 + authToken.Length + 2 + 24;
            byte[] headerLenBytes = BitConverter.GetBytes((ushort)headerLen);
            if (BitConverter.IsLittleEndian) headerLenBytes = headerLenBytes.Reverse().ToArray();

            m.Write(headerLenBytes, 0, headerLenBytes.Length);

            // write 2 bytes that i am not sure about
            m.WriteByte(131);
            m.WriteByte(1);

            // write auth token len
            m.WriteByte(89); // cbor ushort
            var authLenBytes = BitConverter.GetBytes((ushort)authToken.Length);
            if (BitConverter.IsLittleEndian) authLenBytes = authLenBytes.Reverse().ToArray();
            m.Write(authLenBytes, 0, authLenBytes.Length);

            // write actual auth token
            var authBytes = Encoding.UTF8.GetBytes(authToken);
            m.Write(authBytes, 0, authBytes.Length);

            // write nonce
            m.WriteByte(88); // cbor byte
            m.WriteByte(24); // nonce length

            // generate it
            byte[] nonce = new byte[24];
            new Random().NextBytes(nonce);

            // write nonce bytes
            m.Write(nonce, 0, nonce.Length);

            // get additional data
            var additionalData = GenerateAdditionalData(authToken, true);

            // prep payload
            MemoryStream msm = new();
            msm.WriteByte(88);
            msm.WriteByte(0);

            msm.Write(message);

            var encryptedBytes = SecretAeadXChaCha20Poly1305.Encrypt(msm.ToArray(), nonce, key.Keys.TransferSharedSecret, [.. additionalData]);

            // write encrypted data
            m.Write(encryptedBytes);

            byte[] data = m.ToArray();
            //File.WriteAllBytes("our-encryption", data);

            // we are done
            return data;
        }

        private static byte[] GenerateAdditionalData(string authToken, bool encrypting)
        {
            // Generate "additional data" which consists of auth token and its length using cbor.
            // Not sure what the first bytes are but they are always the same. 89 represents start of UShort
            MemoryStream additionalData = new();
            additionalData.WriteByte((byte)(encrypting ? 129 : 130));
            additionalData.WriteByte(1);

            // write auth token len
            if (!encrypting)
            {
                var authLen = BitConverter.GetBytes((ushort)authToken.Length);
                if (BitConverter.IsLittleEndian)
                    authLen = authLen.Reverse().ToArray();
                additionalData.WriteByte(89);
                additionalData.Write(authLen, 0, authLen.Length);

                // write our authentication token
                var authBytes = Encoding.UTF8.GetBytes(authToken);
                additionalData.Write(authBytes, 0, authBytes.Length);
            }

            return additionalData.ToArray();
        }

        private static string CBorReadString(Stream s)
        {
            CBorItem item = CBorReadItem(s);
            if (item.MajorType != 2)
            {
                throw new Exception("invalid string");
            }
            string resp = "";

            var len = item.FullValue;
            for (int i = 0; i < len; i++)
            {
                var b = s.ReadByte();
                if (b == -1) throw new EndOfStreamException();

                resp += (char)b;
            }

            return resp;
        }
        private static byte[] CBorReadByteString(Stream s)
        {
            CBorItem item = CBorReadItem(s);
            if (item.MajorType != 2)
            {
                throw new Exception("invalid string");
            }

            var len = item.FullValue;
            byte[] resp = new byte[len];

            for (int i = 0; i < len; i++)
            {
                var b = s.ReadByte();
                if (b == -1) throw new EndOfStreamException();

                resp[i] = (byte)b;
            }

            return resp;
        }
        private static CBorItem CBorReadItem(Stream s)
        {
            var b = s.ReadByte();
            var type = b & 0x1f;
            CBorItem res = new()
            {
                MajorType = (b >> 5) & 7
            };
            switch (type)
            {
                case 24:
                    // byte
                    res.ByteValue = [(byte)s.ReadByte()];
                    res.type = CBorItemType.Byte;

                    res.FullValue = res.ByteValue[0];
                    break;
                case 25:
                    byte[] arr = new byte[2];
                    ReadToBuff(s, arr);

                    if (BitConverter.IsLittleEndian)
                        Array.Reverse(arr);

                    res.ByteValue = arr;
                    res.type = CBorItemType.UShort;
                    res.UShortValue = BitConverter.ToUInt16(arr, 0);

                    res.FullValue = res.UShortValue;
                    break;
                default:
                    // throw new NotImplementedException();
                    break;
            }

            return res;
        }

        private static void ReadToBuff(Stream s, byte[] buf)
        {
            int i = 0;
            while (i != buf.Length)
            {
                if (i > buf.Length)
                    throw new ArgumentOutOfRangeException(nameof(buf));
                var read = s.Read(buf, i, buf.Length - i);
                if (read == 0)
                    break;
                i += read;
            }

            if (i < buf.Length)
            {
                throw new EndOfStreamException();
            }
        }
    }
    public class PacketDecryptResponse
    {
        public ulong UserId;
        public string UsedAuthToken = "";
        public byte[] Contents = [];
    }
    public class CBorItem
    {
        public CBorItemType type;
        public byte[] ByteValue = [];
        public ushort UShortValue;
        public int MajorType;

        public int FullValue;
    }
    public enum CBorItemType
    {
        Byte,
        UShort,
        ByteString,
    }
}
