#nullable enable
using System;
using System.Collections.Generic;
using System.Text;
using IGP.Multiplayer.Models;
using IGP.Multiplayer.Network;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace IGP.Multiplayer.Protocol
{
    internal sealed class IGPDataPlanePeerTable
    {
        public IGPDataPlanePeerTable(
            byte playerSlot,
            byte hostSlot,
            IReadOnlyDictionary<string, byte> slotByPlayerId,
            IReadOnlyDictionary<byte, string> playerIdBySlot)
        {
            PlayerSlot = playerSlot;
            HostSlot = hostSlot;
            var slots = new Dictionary<string, byte>(StringComparer.Ordinal);
            foreach (KeyValuePair<string, byte> entry in slotByPlayerId) slots.Add(entry.Key, entry.Value);
            var players = new Dictionary<byte, string>();
            foreach (KeyValuePair<byte, string> entry in playerIdBySlot) players.Add(entry.Key, entry.Value);
            SlotByPlayerId = slots;
            PlayerIdBySlot = players;
        }

        public byte PlayerSlot { get; }
        public byte HostSlot { get; }
        public IReadOnlyDictionary<string, byte> SlotByPlayerId { get; }
        public IReadOnlyDictionary<byte, string> PlayerIdBySlot { get; }

        public static IGPDataPlanePeerTable FromHandshakeAck(JObject ack, string expectedPlayerId)
        {
            int playerSlotValue = ack.Value<int?>("playerSlot") ?? 0;
            int hostSlotValue = ack.Value<int?>("hostSlot") ?? 0;
            if (playerSlotValue < 1 || playerSlotValue > IGPDataPlaneEnvelopeCodec.MaxPlayerSlot ||
                hostSlotValue != 1 ||
                !(ack["playerSlots"] is JArray slots) ||
                slots.Count == 0 ||
                slots.Count > IGPDataPlaneEnvelopeCodec.MaxPlayerSlot)
            {
                throw new InvalidOperationException("DataPlaneEnvelope V1 slot metadata is invalid.");
            }

            var nextSlotByPlayerId = new Dictionary<string, byte>(StringComparer.Ordinal);
            var nextPlayerIdBySlot = new Dictionary<byte, string>();
            foreach (JToken entry in slots)
            {
                string? playerId = entry.Value<string>("playerId");
                int slotValue = entry.Value<int?>("slot") ?? 0;
                if (string.IsNullOrWhiteSpace(playerId) ||
                    slotValue < 1 ||
                    slotValue > IGPDataPlaneEnvelopeCodec.MaxPlayerSlot)
                {
                    throw new InvalidOperationException("DataPlaneEnvelope V1 roster entry is invalid.");
                }

                byte slot = (byte)slotValue;
                if (nextSlotByPlayerId.ContainsKey(playerId!) || nextPlayerIdBySlot.ContainsKey(slot))
                {
                    throw new InvalidOperationException("DataPlaneEnvelope V1 roster contains duplicate entries.");
                }

                nextSlotByPlayerId.Add(playerId!, slot);
                nextPlayerIdBySlot.Add(slot, playerId!);
            }

            byte playerSlot = (byte)playerSlotValue;
            byte hostSlot = (byte)hostSlotValue;
            if (!nextPlayerIdBySlot.TryGetValue(playerSlot, out string? authenticatedPlayerId) ||
                !string.Equals(authenticatedPlayerId, expectedPlayerId, StringComparison.Ordinal) ||
                !nextPlayerIdBySlot.ContainsKey(hostSlot))
            {
                throw new InvalidOperationException("DataPlaneEnvelope V1 roster does not contain the authenticated players.");
            }

            return new IGPDataPlanePeerTable(playerSlot, hostSlot, nextSlotByPlayerId, nextPlayerIdBySlot);
        }
    }

    internal static class IGPDataPlaneMessageCodec
    {
        // Base header + mask route + custom type length and its maximum UTF-8 bytes.
        public const int MaxEnvelopeHeaderBytes = 4 + 8 + 1 + byte.MaxValue;

        public static byte[] Encode(Message message, IGPDataPlanePeerTable peers)
        {
            if (message == null) throw new ArgumentNullException(nameof(message));
            if (peers == null) throw new ArgumentNullException(nameof(peers));

            string targetPlayerId = message.targetPlayerId ?? string.Empty;
            P2PMessagePayload? p2p = null;
            if (string.Equals(message.type, "p2p_data", StringComparison.Ordinal))
            {
                p2p = message.content as P2PMessagePayload;
                if (p2p == null && message.content is JObject objectContent)
                {
                    p2p = objectContent.ToObject<P2PMessagePayload>();
                }
                if (!string.IsNullOrWhiteSpace(p2p?.targetId)) targetPlayerId = p2p.targetId;
            }

            ResolveRoute(message, targetPlayerId, peers, out IGPDataPlaneRouteKind route, out byte targetSlot, out ulong targetMask);
            IGPDataPlaneMessageKind kind = IGPDataPlaneEnvelopeCodec.MessageKindForType(message.type, out string customType);
            var envelope = new IGPDataPlaneEnvelope
            {
                MessageKind = kind,
                CustomType = customType,
                RouteKind = route,
                SenderSlot = peers.PlayerSlot,
                TargetSlot = targetSlot,
                TargetMask = targetMask,
            };

            if (kind == IGPDataPlaneMessageKind.Ping || kind == IGPDataPlaneMessageKind.Pong)
            {
                if (!(message.content is IGPReliableHeartbeatPayload heartbeatPayload))
                {
                    throw new InvalidOperationException("Reliable heartbeat payload is missing.");
                }
                envelope.Payload = IGPReliableHeartbeatPayloadCodec.Encode(heartbeatPayload);
            }
            else if (kind == IGPDataPlaneMessageKind.P2PData)
            {
                if (p2p == null || string.IsNullOrWhiteSpace(p2p.data))
                {
                    throw new InvalidOperationException("P2P payload is missing.");
                }

                envelope.Payload = Convert.FromBase64String(p2p.data);
                envelope.ApplicationMessageType = p2p.messageType;
                if (HasReliableChunkMetadata(p2p))
                {
                    if (!HasCompleteReliableChunkMetadata(p2p))
                    {
                        throw new InvalidOperationException("P2P fragment metadata is incomplete.");
                    }

                    envelope.Flags |= IGPDataPlaneEnvelopeFlags.Fragmented;
                    envelope.ApplicationMessageType = p2p.reliableMessageType!.Value;
                    envelope.Fragment = new IGPDataPlaneFragment
                    {
                        Id = ParseFragmentId(p2p.reliableMessageId!),
                        Index = checked((ushort)p2p.reliableChunkIndex!.Value),
                        Count = checked((ushort)p2p.reliableChunkCount!.Value),
                        TotalBytes = checked((uint)p2p.reliableTotalBytes!.Value),
                    };
                }
            }
            else
            {
                envelope.Flags = IGPDataPlaneEnvelopeFlags.Json;
                envelope.Payload = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message.content));
            }

            return IGPDataPlaneEnvelopeCodec.Encode(envelope);
        }

        public static Message Decode(
            byte[] payload,
            string roomId,
            IGPRealtimeTransport transport,
            IGPDataPlanePeerTable peers)
        {
            if (payload == null) throw new ArgumentNullException(nameof(payload));
            if (peers == null) throw new ArgumentNullException(nameof(peers));

            IGPDataPlaneEnvelope envelope = IGPDataPlaneEnvelopeCodec.Decode(payload);
            if (transport == IGPRealtimeTransport.Udp && envelope.Fragment != null)
            {
                throw new InvalidOperationException("UDP does not accept reliable fragment metadata.");
            }
            string type = IGPDataPlaneEnvelopeCodec.TypeForMessageKind(envelope.MessageKind, envelope.CustomType);
            string senderId = peers.PlayerIdBySlot.TryGetValue(envelope.SenderSlot, out string? sender)
                ? sender
                : string.Empty;
            string targetId = envelope.RouteKind == IGPDataPlaneRouteKind.Direct &&
                              peers.PlayerIdBySlot.TryGetValue(envelope.TargetSlot, out string? direct)
                ? direct
                : envelope.RouteKind == IGPDataPlaneRouteKind.ToHost &&
                  peers.PlayerIdBySlot.TryGetValue(peers.HostSlot, out string? host)
                    ? host
                    : string.Empty;

            object? content;
            if (envelope.MessageKind == IGPDataPlaneMessageKind.Ping ||
                envelope.MessageKind == IGPDataPlaneMessageKind.Pong)
            {
                if (envelope.Flags == IGPDataPlaneEnvelopeFlags.None)
                {
                    content = IGPReliableHeartbeatPayloadCodec.Decode(envelope.Payload);
                }
                else if (envelope.Flags == IGPDataPlaneEnvelopeFlags.Json)
                {
                    content = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(envelope.Payload));
                }
                else
                {
                    throw new InvalidOperationException("Reliable heartbeat payload flags are invalid.");
                }
            }
            else if (envelope.MessageKind == IGPDataPlaneMessageKind.P2PData)
            {
                var p2p = new P2PMessagePayload
                {
                    senderId = senderId,
                    targetId = targetId,
                    data = Convert.ToBase64String(envelope.Payload),
                    messageType = envelope.ApplicationMessageType,
                    transportChannel = IGPP2PTransportChannels.Data,
                    reliable = transport != IGPRealtimeTransport.Udp,
                };
                if (envelope.Fragment != null)
                {
                    p2p.reliableMessageId = BitConverter.ToString(envelope.Fragment.Id).Replace("-", string.Empty).ToLowerInvariant();
                    p2p.reliableChunkIndex = envelope.Fragment.Index;
                    p2p.reliableChunkCount = envelope.Fragment.Count;
                    p2p.reliableTotalBytes = checked((int)envelope.Fragment.TotalBytes);
                    p2p.reliableMessageType = envelope.ApplicationMessageType;
                }
                content = p2p;
            }
            else
            {
                content = (envelope.Flags & IGPDataPlaneEnvelopeFlags.Json) != 0
                    ? JsonConvert.DeserializeObject(Encoding.UTF8.GetString(envelope.Payload))
                    : envelope.Payload;
            }

            return new Message
            {
                type = type,
                roomId = roomId ?? string.Empty,
                playerId = senderId,
                targetPlayerId = targetId,
                reliable = transport != IGPRealtimeTransport.Udp,
                content = content,
            };
        }

        private static void ResolveRoute(
            Message message,
            string targetPlayerId,
            IGPDataPlanePeerTable peers,
            out IGPDataPlaneRouteKind route,
            out byte targetSlot,
            out ulong targetMask)
        {
            targetSlot = 0;
            targetMask = 0;
            if (message.dataPlaneTargetPlayerIds != null)
            {
                foreach (string playerId in message.dataPlaneTargetPlayerIds)
                {
                    if (!peers.SlotByPlayerId.TryGetValue(playerId, out byte slot))
                    {
                        throw new InvalidOperationException("Target player has no data-plane slot.");
                    }
                    targetMask |= 1UL << (slot - 1);
                }

                if (targetMask == 0) throw new InvalidOperationException("Data-plane target mask is empty.");
                ulong allOthers = 0;
                foreach (byte slot in peers.SlotByPlayerId.Values)
                {
                    if (slot != peers.PlayerSlot) allOthers |= 1UL << (slot - 1);
                }
                if (targetMask == allOthers)
                {
                    route = IGPDataPlaneRouteKind.Others;
                    targetMask = 0;
                    return;
                }
                route = IGPDataPlaneRouteKind.Mask64;
                return;
            }

            if (string.IsNullOrWhiteSpace(targetPlayerId))
            {
                route = IGPDataPlaneRouteKind.Others;
                return;
            }
            if (!peers.SlotByPlayerId.TryGetValue(targetPlayerId, out targetSlot))
            {
                throw new InvalidOperationException("Target player has no data-plane slot.");
            }
            if (targetSlot == peers.HostSlot)
            {
                route = IGPDataPlaneRouteKind.ToHost;
                targetSlot = 0;
                return;
            }
            route = IGPDataPlaneRouteKind.Direct;
        }

        private static bool HasReliableChunkMetadata(P2PMessagePayload payload) =>
            !string.IsNullOrWhiteSpace(payload.reliableMessageId) ||
            payload.reliableChunkIndex.HasValue ||
            payload.reliableChunkCount.HasValue ||
            payload.reliableTotalBytes.HasValue ||
            payload.reliableMessageType.HasValue;

        private static bool HasCompleteReliableChunkMetadata(P2PMessagePayload payload) =>
            !string.IsNullOrWhiteSpace(payload.reliableMessageId) &&
            payload.reliableChunkIndex.HasValue &&
            payload.reliableChunkCount.HasValue &&
            payload.reliableTotalBytes.HasValue &&
            payload.reliableMessageType.HasValue;

        private static byte[] ParseFragmentId(string value)
        {
            string compact = Guid.Parse(value).ToString("N");
            var result = new byte[16];
            for (int i = 0; i < result.Length; i++)
            {
                result[i] = Convert.ToByte(compact.Substring(i * 2, 2), 16);
            }
            return result;
        }
    }
}
