#nullable enable
using System;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;

namespace IGP.UnitySDK
{
    internal sealed class IGPMobileHostSessionTransport : IIGPHostSessionTransport
    {
        private const int MaxFramePayloadBytes = 16 * 1024 * 1024;
        private const int MaxCarrierCharacters = ((MaxFramePayloadBytes + 2) / 3 * 4) + 512;
        private const int CarrierVersion = 1;
        private const string CarrierType = "IgpHostFrame";

        private readonly IGPMobileHostSessionBridge bridge;

        internal IGPMobileHostSessionTransport(IGPMobileHostSessionBridge bridge)
        {
            this.bridge = bridge ?? throw new ArgumentNullException(nameof(bridge));
        }

        public bool IsConnected { get; private set; }

        public event Action<byte[]>? PayloadReceived;
        public event Action<Exception>? Failed;
        public event Action<string>? Closed;

        public async Task ConnectAsync(CancellationToken cancellationToken)
        {
            await DisconnectAsync();
            await bridge.WaitUntilReadyAsync(cancellationToken);
            cancellationToken.ThrowIfCancellationRequested();
            IsConnected = true;
            if (!bridge.TryOpen(HandleFrameReceived, HandleClosed))
            {
                IsConnected = false;
                throw new IGPSDKException(
                    "Mobile Host Session transport closed while connecting",
                    Models.IGPErrorCodes.ERR_DESKTOP_SESSION_REQUIRED);
            }
        }

        public Task SendAsync(byte[] payload, CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (!IsConnected)
            {
                throw new IGPSDKException(
                    "Mobile Host Session transport is not connected",
                    Models.IGPErrorCodes.ERR_DESKTOP_SESSION_REQUIRED);
            }

            if (payload == null || payload.Length == 0 || payload.Length > MaxFramePayloadBytes)
            {
                throw new IGPSDKException("Invalid Mobile Host Session payload length");
            }

            var frame = new MobileHostFrame
            {
                version = CarrierVersion,
                type = CarrierType,
                payload = new MobileHostFramePayload
                {
                    protobufBase64 = Convert.ToBase64String(payload),
                },
            };
            bridge.SendFrame(JsonConvert.SerializeObject(frame));
            return Task.CompletedTask;
        }

        public Task DisconnectAsync()
        {
            if (IsConnected)
            {
                IsConnected = false;
                bridge.Detach(HandleFrameReceived, HandleClosed);
            }

            return Task.CompletedTask;
        }

        public void Dispose()
        {
            _ = DisconnectAsync();
        }

        private void HandleFrameReceived(string frameJson)
        {
            if (!IsConnected)
            {
                return;
            }

            try
            {
                if (frameJson.Length > MaxCarrierCharacters)
                {
                    throw new IGPSDKException("Mobile Host Session frame is too large");
                }

                var frame = JsonConvert.DeserializeObject<MobileHostFrame>(frameJson);
                if (frame == null ||
                    frame.version != CarrierVersion ||
                    !string.Equals(frame.type, CarrierType, StringComparison.Ordinal) ||
                    frame.payload == null ||
                    string.IsNullOrWhiteSpace(frame.payload.protobufBase64))
                {
                    throw new IGPSDKException("Invalid Mobile Host Session frame");
                }

                byte[] payload;
                try
                {
                    payload = Convert.FromBase64String(frame.payload.protobufBase64);
                }
                catch (FormatException exception)
                {
                    throw new IGPSDKException("Invalid Mobile Host Session protobuf payload", exception);
                }

                if (payload.Length == 0 || payload.Length > MaxFramePayloadBytes)
                {
                    throw new IGPSDKException("Invalid Mobile Host Session payload length");
                }

                PayloadReceived?.Invoke(payload);
            }
            catch (Exception exception)
            {
                Failed?.Invoke(exception);
                HandleClosed("mobile-host-session-protocol-error");
            }
        }

        private void HandleClosed(string reason)
        {
            if (!IsConnected)
            {
                return;
            }

            IsConnected = false;
            bridge.Detach(HandleFrameReceived, HandleClosed);
            Closed?.Invoke(reason);
        }

        [Serializable]
        private sealed class MobileHostFrame
        {
            public int version;
            public string type = string.Empty;
            public MobileHostFramePayload? payload;
        }

        [Serializable]
        private sealed class MobileHostFramePayload
        {
            public string protobufBase64 = string.Empty;
        }
    }
}
