#nullable enable
using System;
using Newtonsoft.Json;

namespace IGP.Multiplayer
{
    public sealed class IGPMultiplayerEndpoint
    {
        public string Host { get; }
        public int Port { get; }
        public int? FrameMaxBytes { get; }
        public int? PayloadMaxBytes { get; }

        public IGPMultiplayerEndpoint(
            string host,
            int port,
            int? frameMaxBytes = null,
            int? payloadMaxBytes = null)
        {
            Host = RequireText(host, nameof(host));
            if (port < 1 || port > ushort.MaxValue) throw new ArgumentOutOfRangeException(nameof(port));
            if (frameMaxBytes <= 0) throw new ArgumentOutOfRangeException(nameof(frameMaxBytes));
            if (payloadMaxBytes <= 0) throw new ArgumentOutOfRangeException(nameof(payloadMaxBytes));
            Port = port;
            FrameMaxBytes = frameMaxBytes;
            PayloadMaxBytes = payloadMaxBytes;
        }

        public override string ToString() => $"IGPMultiplayerEndpoint(Host={Host}, Port={Port})";

        private static string RequireText(string value, string parameterName)
        {
            if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException("Value is required", parameterName);
            return value.Trim();
        }
    }

    public sealed class IGPMultiplayerDescriptor
    {
        public string RoomId { get; }
        public string PlayerId { get; }
        internal string Token { get; }
        public long ExpiresAtUnixSeconds { get; }
        public IGPMultiplayerEndpoint Kcp { get; }
        public IGPMultiplayerEndpoint? Tcp { get; }
        public IGPMultiplayerEndpoint? UnreliableUdp { get; }

        public IGPMultiplayerDescriptor(
            string roomId,
            string playerId,
            string token,
            long expiresAtUnixSeconds,
            IGPMultiplayerEndpoint kcp,
            IGPMultiplayerEndpoint? tcp = null,
            IGPMultiplayerEndpoint? unreliableUdp = null)
        {
            RoomId = RequireText(roomId, nameof(roomId));
            PlayerId = RequireText(playerId, nameof(playerId));
            Token = RequireText(token, nameof(token));
            if (expiresAtUnixSeconds <= 0) throw new ArgumentOutOfRangeException(nameof(expiresAtUnixSeconds));
            ExpiresAtUnixSeconds = expiresAtUnixSeconds;
            Kcp = kcp ?? throw new ArgumentNullException(nameof(kcp));
            Tcp = tcp;
            UnreliableUdp = unreliableUdp;
        }

        public static IGPMultiplayerDescriptor FromSdkDescriptorJson(
            string roomId,
            string playerId,
            string sdkDescriptorJson)
        {
            if (string.IsNullOrWhiteSpace(sdkDescriptorJson))
                throw new ArgumentException("SDK descriptor JSON is required", nameof(sdkDescriptorJson));

            WireDescriptor? wire;
            try
            {
                wire = JsonConvert.DeserializeObject<WireDescriptor>(sdkDescriptorJson);
            }
            catch (JsonException exception)
            {
                throw new ArgumentException("SDK descriptor JSON is invalid", nameof(sdkDescriptorJson), exception);
            }

            if (wire == null || wire.Version != 1 || wire.Transports?.Kcp == null)
                throw new ArgumentException("SDK descriptor JSON has an unsupported shape", nameof(sdkDescriptorJson));

            return new IGPMultiplayerDescriptor(
                roomId,
                playerId,
                wire.Token,
                wire.ExpiresAtUnixSeconds,
                ToEndpoint(wire.Transports.Kcp, nameof(wire.Transports.Kcp)),
                ToOptionalEndpoint(wire.Transports.Tcp),
                ToOptionalEndpoint(wire.Transports.RawUdp));
        }

        public override string ToString() =>
            $"IGPMultiplayerDescriptor(RoomId={RoomId}, PlayerId={PlayerId}, ExpiresAtUnixSeconds={ExpiresAtUnixSeconds}, Credential=[redacted])";

        internal IGPHostSessionDataPlaneDescriptor ToHostedDescriptor()
        {
            if (ExpiresAtUnixSeconds <= DateTimeOffset.UtcNow.ToUnixTimeSeconds())
                throw new ArgumentException("Multiplayer descriptor has expired", nameof(ExpiresAtUnixSeconds));

            return new IGPHostSessionDataPlaneDescriptor
            {
                Mode = IGPHostedDataPlaneMode.DirectKcp,
                Token = Token,
                Host = Kcp.Host,
                Port = checked((uint)Kcp.Port),
                ExpiresAtUnixSeconds = ExpiresAtUnixSeconds,
                TcpHost = Tcp?.Host ?? string.Empty,
                TcpPort = checked((uint)(Tcp?.Port ?? 0)),
                TcpFrameMaxBytes = Tcp?.FrameMaxBytes,
                unreliableUdpHost = UnreliableUdp?.Host ?? string.Empty,
                unreliableUdpPort = checked((uint)(UnreliableUdp?.Port ?? 0)),
                unreliableUdpToken = UnreliableUdp == null ? string.Empty : Token,
                unreliableUdpPayloadMaxBytes = UnreliableUdp?.PayloadMaxBytes,
            };
        }

        private static IGPMultiplayerEndpoint ToEndpoint(WireEndpoint endpoint, string parameterName)
        {
            try
            {
                return new IGPMultiplayerEndpoint(
                    endpoint.Host,
                    endpoint.Port,
                    endpoint.FrameMaxBytes > 0 ? endpoint.FrameMaxBytes : null,
                    endpoint.PayloadMaxBytes > 0 ? endpoint.PayloadMaxBytes : null);
            }
            catch (ArgumentException exception)
            {
                throw new ArgumentException("SDK descriptor endpoint is invalid", parameterName, exception);
            }
        }

        private static IGPMultiplayerEndpoint? ToOptionalEndpoint(WireEndpoint? endpoint) =>
            endpoint == null || string.IsNullOrWhiteSpace(endpoint.Host) || endpoint.Port <= 0
                ? null
                : ToEndpoint(endpoint, nameof(endpoint));

        private static string RequireText(string value, string parameterName)
        {
            if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException("Value is required", parameterName);
            return value.Trim();
        }

        private sealed class WireDescriptor
        {
            [JsonProperty("version")]
            public int Version { get; set; }

            [JsonProperty("token")]
            public string Token { get; set; } = string.Empty;

            [JsonProperty("expiresAtUnixSeconds")]
            public long ExpiresAtUnixSeconds { get; set; }

            [JsonProperty("transports")]
            public WireTransports? Transports { get; set; }
        }

        private sealed class WireTransports
        {
            [JsonProperty("kcp")]
            public WireEndpoint? Kcp { get; set; }

            [JsonProperty("tcp")]
            public WireEndpoint? Tcp { get; set; }

            [JsonProperty("rawUdp")]
            public WireEndpoint? RawUdp { get; set; }
        }

        private sealed class WireEndpoint
        {
            [JsonProperty("host")]
            public string Host { get; set; } = string.Empty;

            [JsonProperty("port")]
            public int Port { get; set; }

            [JsonProperty("frameMaxBytes")]
            public int FrameMaxBytes { get; set; }

            [JsonProperty("payloadMaxBytes")]
            public int PayloadMaxBytes { get; set; }
        }
    }
}
