#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipes;
using System.Threading;
using System.Threading.Tasks;

using IGP.UnitySDK.Models;
using Newtonsoft.Json;

namespace IGP.UnitySDK
{
    /// <summary>
    /// Long-lived Windows named pipe client used for hosted IGP control-plane
    /// operations and room snapshot delivery.
    /// </summary>
    public sealed class IGPHostSessionClient : IDisposable
    {
        private const int ConnectTimeoutMs = 5000;
        private const int MaxFramePayloadBytes = 8 * 1024 * 1024;

        private readonly object syncRoot = new object();
        private readonly string pipeEndpoint;
        private readonly string secret;
        private readonly SemaphoreSlim writeLock = new SemaphoreSlim(1, 1);
        private readonly Dictionary<string, TaskCompletionSource<IGPHostSessionCommandResult>> pendingRequests =
            new Dictionary<string, TaskCompletionSource<IGPHostSessionCommandResult>>();
        private readonly Dictionary<string, TaskCompletionSource<IGPHostSessionDataPlaneResponse>> pendingDataPlaneRequests =
            new Dictionary<string, TaskCompletionSource<IGPHostSessionDataPlaneResponse>>();

        private NamedPipeClientStream? pipe;
        private CancellationTokenSource? lifetimeCts;
        private Task? readLoopTask;
        private TaskCompletionSource<bool>? attachCompletionSource;
        private string attachedRoomId = string.Empty;
        private string attachedPlayerId = string.Empty;
        private bool attachAcknowledged;
        private bool initialSnapshotReceived;

        public IGPHostSessionClient(string pipeEndpoint, string secret)
        {
            this.pipeEndpoint = pipeEndpoint ?? string.Empty;
            this.secret = secret ?? string.Empty;
        }

        public bool IsAttached { get; private set; }

        public event Action<IGPHostSessionSnapshotEvent>? RoomSnapshotReceived;
        public event Action<IGPHostSessionRoomEvent>? RoomEventReceived;
        public event Action<Message>? MessageReceived;
        public event Action<string>? Detached;
        public event Action<string, string>? ErrorOccurred;

        public async Task ConnectAsync(
            string roomId,
            string playerId,
            CancellationToken cancellationToken = default)
        {
            if (string.IsNullOrWhiteSpace(roomId))
            {
                throw new ArgumentException("Room ID is required", nameof(roomId));
            }

            if (string.IsNullOrWhiteSpace(playerId))
            {
                throw new ArgumentException("Player ID is required", nameof(playerId));
            }

            if (string.IsNullOrWhiteSpace(pipeEndpoint))
            {
                throw new IGPSDKException("IGP host session endpoint is missing");
            }

            if (string.IsNullOrWhiteSpace(secret))
            {
                throw new IGPSDKException("IGP host session secret is missing");
            }

            await DisconnectAsync();

            var pipeName = ExtractPipeName(pipeEndpoint);
            pipe = new NamedPipeClientStream(
                ".",
                pipeName,
                PipeDirection.InOut,
                PipeOptions.Asynchronous);
            lifetimeCts = new CancellationTokenSource();
            attachCompletionSource = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
            attachAcknowledged = false;
            initialSnapshotReceived = false;

            try
            {
                await Task.Run(() => pipe.Connect(ConnectTimeoutMs), cancellationToken);
                cancellationToken.ThrowIfCancellationRequested();
                attachedRoomId = roomId;
                attachedPlayerId = playerId;

                readLoopTask = ReadLoopAsync(pipe, lifetimeCts.Token);

                var attachPayload = IGPHostSessionProtocol.EncodeAttachRequest(secret, roomId, playerId);
                await WriteFrameAsync(attachPayload, cancellationToken);

                using (cancellationToken.Register(() => attachCompletionSource.TrySetCanceled(cancellationToken)))
                {
                    await attachCompletionSource.Task;
                }
            }
            catch
            {
                await DisconnectAsync();
                throw;
            }
        }

        public async Task<IGPHostSessionCommandResult> SetReadyAsync(
            bool isReady,
            CancellationToken cancellationToken = default)
        {
            return await SendCommandAsync(
                IGPHostSessionCommandType.SetReady,
                cancellationToken,
                boolValue: isReady);
        }

        public async Task<IGPHostSessionCommandResult> LeaveRoomAsync(
            CancellationToken cancellationToken = default)
        {
            return await SendCommandAsync(IGPHostSessionCommandType.LeaveRoom, cancellationToken);
        }

        public async Task<IGPHostSessionCommandResult> StartGameAsync(
            CancellationToken cancellationToken = default)
        {
            return await SendCommandAsync(IGPHostSessionCommandType.StartGame, cancellationToken);
        }

        public async Task<IGPHostSessionCommandResult> FinishGameAsync(
            CancellationToken cancellationToken = default)
        {
            return await SendCommandAsync(IGPHostSessionCommandType.FinishGame, cancellationToken);
        }

        public async Task<IGPHostSessionCommandResult> RematchGameAsync(
            CancellationToken cancellationToken = default)
        {
            return await SendCommandAsync(IGPHostSessionCommandType.RematchGame, cancellationToken);
        }

        public async Task<IGPHostSessionCommandResult> ChangeTeamAsync(
            string teamId,
            CancellationToken cancellationToken = default)
        {
            if (string.IsNullOrWhiteSpace(teamId))
            {
                throw new ArgumentException("Team ID is required", nameof(teamId));
            }

            return await SendCommandAsync(
                IGPHostSessionCommandType.ChangeTeam,
                cancellationToken,
                stringValue: teamId);
        }

        public async Task<IGPHostSessionCommandResult> RefreshRoomAsync(
            CancellationToken cancellationToken = default)
        {
            return await SendCommandAsync(IGPHostSessionCommandType.RefreshRoom, cancellationToken);
        }

        public async Task<IGPHostSessionCommandResult> ClearAchievementsAsync(
            CancellationToken cancellationToken = default)
        {
            return await SendCommandAsync(IGPHostSessionCommandType.ClearAchievements, cancellationToken);
        }

        public async Task<IGPHostSessionCommandResult> SendMessageAsync(
            Message message,
            CancellationToken cancellationToken = default)
        {
            if (message == null)
            {
                throw new ArgumentNullException(nameof(message));
            }

            if (string.IsNullOrWhiteSpace(message.type))
            {
                throw new ArgumentException("Message type is required", nameof(message));
            }

            if (IGPReservedMessageTypes.IsReserved(message.type))
            {
                throw new IGPSDKException("Message type is reserved; use the explicit hosted control API instead");
            }

            var contentJson = message.content == null
                ? null
                : JsonConvert.SerializeObject(message.content);

            return await SendCommandAsync(
                IGPHostSessionCommandType.SendMessage,
                cancellationToken,
                stringValue: null,
                messageType: message.type,
                targetPlayerId: message.targetPlayerId,
                contentJson: contentJson,
                reliable: message.reliable,
                timestamp: message.timestamp);
        }

        public async Task SendPingAsync(CancellationToken cancellationToken = default)
        {
            await SendCommandAsync(
                IGPHostSessionCommandType.SendPing,
                cancellationToken);
        }

        public async Task SetStateAsync(
            string scope,
            string key,
            object? value,
            string? targetPlayerId = null,
            bool reliable = true,
            CancellationToken cancellationToken = default)
        {
            await SendCommandAsync(
                IGPHostSessionCommandType.SetState,
                cancellationToken,
                targetPlayerId: targetPlayerId,
                contentJson: JsonConvert.SerializeObject(value),
                reliable: reliable,
                scope: scope,
                key: key);
        }

        public async Task GetStateAsync(
            string scope,
            string key,
            string? targetPlayerId = null,
            CancellationToken cancellationToken = default)
        {
            await SendCommandAsync(
                IGPHostSessionCommandType.GetState,
                cancellationToken,
                targetPlayerId: targetPlayerId,
                scope: scope,
                key: key);
        }

        public async Task ResetStateAsync(
            string scope,
            string[]? keysToExclude = null,
            CancellationToken cancellationToken = default)
        {
            await SendCommandAsync(
                IGPHostSessionCommandType.ResetState,
                cancellationToken,
                contentJson: JsonConvert.SerializeObject(keysToExclude ?? Array.Empty<string>()),
                scope: scope);
        }

        public async Task RegisterRPCAsync(
            string name,
            CancellationToken cancellationToken = default)
        {
            await SendCommandAsync(
                IGPHostSessionCommandType.RegisterRpc,
                cancellationToken,
                rpcName: name);
        }

        public async Task UnregisterRPCAsync(
            string name,
            CancellationToken cancellationToken = default)
        {
            await SendCommandAsync(
                IGPHostSessionCommandType.UnregisterRpc,
                cancellationToken,
                rpcName: name);
        }

        public async Task<string> CallRPCAsync(
            string name,
            object? data = null,
            string mode = "all",
            string? requestId = null,
            CancellationToken cancellationToken = default)
        {
            requestId ??= Guid.NewGuid().ToString("N");
            await SendCommandAsync(
                IGPHostSessionCommandType.CallRpc,
                cancellationToken,
                stringValue: requestId,
                contentJson: JsonConvert.SerializeObject(data),
                rpcName: name,
                mode: mode);
            return requestId;
        }

        public async Task<IGPHostSessionDataPlaneResponse> RequestDataPlaneAsync(
            CancellationToken cancellationToken = default)
        {
            if (pipe == null || !IsAttached)
            {
                throw new IGPSDKException("IGP host session is not attached");
            }

            var requestId = Guid.NewGuid().ToString("N");
            var completionSource = new TaskCompletionSource<IGPHostSessionDataPlaneResponse>(
                TaskCreationOptions.RunContinuationsAsynchronously);

            lock (syncRoot)
            {
                pendingDataPlaneRequests[requestId] = completionSource;
            }

            try
            {
                var payload = IGPHostSessionProtocol.EncodeCommandRequest(
                    requestId,
                    IGPHostSessionCommandType.RequestDataPlane);
                await WriteFrameAsync(payload, cancellationToken);

                using (cancellationToken.Register(() => completionSource.TrySetCanceled(cancellationToken)))
                {
                    var result = await completionSource.Task;
                    if (!result.Success)
                    {
                        throw new IGPSDKException(string.IsNullOrWhiteSpace(result.Message)
                            ? "Failed to request data plane descriptor from hosted session"
                            : result.Message);
                    }

                    return result;
                }
            }
            catch
            {
                lock (syncRoot)
                {
                    pendingDataPlaneRequests.Remove(requestId);
                }

                throw;
            }
        }

        public async Task DisconnectAsync()
        {
            var localPipe = pipe;
            pipe = null;
            IsAttached = false;
            attachedRoomId = string.Empty;
            attachedPlayerId = string.Empty;
            attachAcknowledged = false;
            initialSnapshotReceived = false;

            if (lifetimeCts != null)
            {
                lifetimeCts.Cancel();
                lifetimeCts.Dispose();
                lifetimeCts = null;
            }

            attachCompletionSource?.TrySetCanceled();
            attachCompletionSource = null;

            FailPendingRequests(new IGPSDKException("IGP host session closed"));
            FailPendingDataPlaneRequests(new IGPSDKException("IGP host session closed"));

            if (localPipe != null)
            {
                try
                {
                    localPipe.Dispose();
                }
                catch
                {
                    // Ignore dispose failures.
                }
            }

            if (readLoopTask != null)
            {
                try
                {
                    await readLoopTask;
                }
                catch
                {
                    // Read loop exceptions are surfaced through events/TCS.
                }
                finally
                {
                    readLoopTask = null;
                }
            }
        }

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

        private async Task<IGPHostSessionCommandResult> SendCommandAsync(
            IGPHostSessionCommandType command,
            CancellationToken cancellationToken,
            bool? boolValue = null,
            string? stringValue = null,
            string? messageType = null,
            string? targetPlayerId = null,
            string? contentJson = null,
            bool? reliable = null,
            string? timestamp = null,
            string? scope = null,
            string? key = null,
            string? rpcName = null,
            string? mode = null)
        {
            if (pipe == null || !IsAttached)
            {
                throw new IGPSDKException("IGP host session is not attached");
            }

            var requestId = Guid.NewGuid().ToString("N");
            var completionSource = new TaskCompletionSource<IGPHostSessionCommandResult>(
                TaskCreationOptions.RunContinuationsAsynchronously);

            lock (syncRoot)
            {
                pendingRequests[requestId] = completionSource;
            }

            try
            {
                var payload = IGPHostSessionProtocol.EncodeCommandRequest(
                    requestId,
                    command,
                    boolValue,
                    stringValue,
                    messageType,
                    targetPlayerId,
                    contentJson,
                    reliable,
                    timestamp,
                    scope,
                    key,
                    rpcName,
                    mode);
                await WriteFrameAsync(payload, cancellationToken);

                using (cancellationToken.Register(() => completionSource.TrySetCanceled(cancellationToken)))
                {
                    var result = await completionSource.Task;
                    if (!result.Success)
                    {
                        throw new IGPSDKException(string.IsNullOrWhiteSpace(result.Message)
                            ? $"Host session command failed: {command}"
                            : result.Message);
                    }

                    return result;
                }
            }
            catch
            {
                lock (syncRoot)
                {
                    pendingRequests.Remove(requestId);
                }

                throw;
            }
        }

        private async Task WriteFrameAsync(byte[] payload, CancellationToken cancellationToken)
        {
            if (pipe == null)
            {
                throw new IGPSDKException("IGP host session pipe is not connected");
            }

            var frame = EncodeFrame(payload);
            await writeLock.WaitAsync(cancellationToken);
            try
            {
                await pipe.WriteAsync(frame, 0, frame.Length, cancellationToken);
            }
            finally
            {
                writeLock.Release();
            }
        }

        private async Task ReadLoopAsync(Stream stream, CancellationToken cancellationToken)
        {
            try
            {
                while (!cancellationToken.IsCancellationRequested)
                {
                    var payload = await ReadFrameAsync(stream, cancellationToken);
                    if (payload == null)
                    {
                        break;
                    }

                    var message = IGPHostSessionProtocol.DecodeServerMessage(payload);
                    HandleServerMessage(message);
                }
            }
            catch (OperationCanceledException)
            {
                // Expected when disconnecting.
            }
            catch (Exception ex)
            {
                attachCompletionSource?.TrySetException(ex);
                FailPendingRequests(ex);
                ErrorOccurred?.Invoke("SESSION_BRIDGE_ERROR", ex.Message);
            }
            finally
            {
                if (IsAttached)
                {
                    IsAttached = false;
                    Detached?.Invoke("session-closed");
                }
            }
        }

        private void HandleServerMessage(IGPHostSessionServerMessage message)
        {
            switch (message.Type)
            {
                case IGPHostSessionServerMessageType.Attached:
                    IsAttached = true;
                    attachAcknowledged = true;
                    if (initialSnapshotReceived)
                    {
                        attachCompletionSource?.TrySetResult(true);
                    }
                    return;
                case IGPHostSessionServerMessageType.RoomSnapshot:
                    if (message.RoomSnapshot != null &&
                        message.RoomSnapshot.Room != null &&
                        message.RoomSnapshot.CurrentPlayer != null)
                    {
                        RoomSnapshotReceived?.Invoke(message.RoomSnapshot);
                        initialSnapshotReceived = true;
                        if (attachAcknowledged)
                        {
                            attachCompletionSource?.TrySetResult(true);
                        }
                    }
                    return;
                case IGPHostSessionServerMessageType.CommandResult:
                    if (message.CommandResult == null)
                    {
                        return;
                    }

                    TaskCompletionSource<IGPHostSessionCommandResult>? requestCompletion = null;
                    lock (syncRoot)
                    {
                        if (pendingRequests.TryGetValue(message.CommandResult.RequestId, out requestCompletion))
                        {
                            pendingRequests.Remove(message.CommandResult.RequestId);
                        }
                    }

                    requestCompletion?.TrySetResult(message.CommandResult);
                    return;
                case IGPHostSessionServerMessageType.Message:
                    if (message.Message == null || string.IsNullOrWhiteSpace(message.Message.MessageType))
                    {
                        return;
                    }

                    MessageReceived?.Invoke(new Message
                    {
                        type = message.Message.MessageType,
                        roomId = string.IsNullOrWhiteSpace(message.Message.RoomId)
                            ? attachedRoomId
                            : message.Message.RoomId,
                        playerId = string.IsNullOrWhiteSpace(message.Message.PlayerId)
                            ? attachedPlayerId
                            : message.Message.PlayerId,
                        targetPlayerId = string.IsNullOrWhiteSpace(message.Message.TargetPlayerId)
                            ? string.Empty
                            : message.Message.TargetPlayerId,
                        content = string.IsNullOrWhiteSpace(message.Message.ContentJson)
                            ? null
                            : JsonConvert.DeserializeObject<object>(message.Message.ContentJson),
                        reliable = message.Message.Reliable ?? true,
                        timestamp = string.IsNullOrWhiteSpace(message.Message.Timestamp)
                            ? string.Empty
                            : message.Message.Timestamp,
                    });
                    return;
                case IGPHostSessionServerMessageType.DataPlane:
                    if (message.DataPlane == null)
                    {
                        return;
                    }

                    TaskCompletionSource<IGPHostSessionDataPlaneResponse>? dataPlaneCompletion = null;
                    lock (syncRoot)
                    {
                        if (pendingDataPlaneRequests.TryGetValue(message.DataPlane.RequestId, out dataPlaneCompletion))
                        {
                            pendingDataPlaneRequests.Remove(message.DataPlane.RequestId);
                        }
                    }

                    dataPlaneCompletion?.TrySetResult(message.DataPlane);
                    return;
                case IGPHostSessionServerMessageType.RoomEvent:
                    if (message.RoomEvent != null)
                    {
                        RoomEventReceived?.Invoke(message.RoomEvent);
                    }
                    return;
                case IGPHostSessionServerMessageType.Detached:
                    IsAttached = false;
                    attachCompletionSource?.TrySetException(
                        new IGPSDKException(message.Detached?.Reason ?? "IGP host session detached"));
                    FailPendingRequests(new IGPSDKException(message.Detached?.Reason ?? "IGP host session detached"));
                    FailPendingDataPlaneRequests(new IGPSDKException(message.Detached?.Reason ?? "IGP host session detached"));
                    Detached?.Invoke(message.Detached?.Reason ?? "session-detached");
                    return;
                case IGPHostSessionServerMessageType.Error:
                    var code = message.Error?.Code ?? "SESSION_ERROR";
                    var errorMessage = message.Error?.Message ?? "IGP host session error";
                    attachCompletionSource?.TrySetException(new IGPSDKException(errorMessage));
                    FailPendingDataPlaneRequests(new IGPSDKException(errorMessage));
                    ErrorOccurred?.Invoke(code, errorMessage);
                    return;
                default:
                    throw new IGPSDKException($"Unsupported host session message type: {message.Type}");
            }
        }

        private void FailPendingRequests(Exception error)
        {
            List<TaskCompletionSource<IGPHostSessionCommandResult>> pending;
            lock (syncRoot)
            {
                pending = new List<TaskCompletionSource<IGPHostSessionCommandResult>>(pendingRequests.Values);
                pendingRequests.Clear();
            }

            foreach (var item in pending)
            {
                item.TrySetException(error);
            }
        }

        private void FailPendingDataPlaneRequests(Exception error)
        {
            List<TaskCompletionSource<IGPHostSessionDataPlaneResponse>> pending;
            lock (syncRoot)
            {
                pending = new List<TaskCompletionSource<IGPHostSessionDataPlaneResponse>>(pendingDataPlaneRequests.Values);
                pendingDataPlaneRequests.Clear();
            }

            foreach (var item in pending)
            {
                item.TrySetException(error);
            }
        }

        private static string ExtractPipeName(string endpoint)
        {
            const string pipePrefix = @"\\.\pipe\";
            if (endpoint.StartsWith(pipePrefix, StringComparison.OrdinalIgnoreCase))
            {
                return endpoint.Substring(pipePrefix.Length);
            }

            return endpoint;
        }

        private static byte[] EncodeFrame(byte[] payload)
        {
            var frame = new byte[4 + payload.Length];
            var lengthBytes = BitConverter.GetBytes(payload.Length);
            Buffer.BlockCopy(lengthBytes, 0, frame, 0, 4);
            Buffer.BlockCopy(payload, 0, frame, 4, payload.Length);
            return frame;
        }

        private static async Task<byte[]?> ReadFrameAsync(Stream stream, CancellationToken cancellationToken)
        {
            var header = await ReadExactAsync(stream, 4, cancellationToken, allowEndOfStream: true);
            if (header == null)
            {
                return null;
            }

            var payloadLength = BitConverter.ToInt32(header, 0);
            if (payloadLength <= 0 || payloadLength > MaxFramePayloadBytes)
            {
                throw new IGPSDKException("Invalid host session frame length");
            }

            return await ReadExactAsync(stream, payloadLength, cancellationToken, allowEndOfStream: false);
        }

        private static async Task<byte[]?> ReadExactAsync(
            Stream stream,
            int length,
            CancellationToken cancellationToken,
            bool allowEndOfStream)
        {
            var buffer = new byte[length];
            var offset = 0;

            while (offset < length)
            {
                var read = await stream.ReadAsync(buffer, offset, length - offset, cancellationToken);
                if (read <= 0)
                {
                    if (offset == 0 && allowEndOfStream)
                    {
                        return null;
                    }

                    throw new IGPSDKException("Unexpected end of host session stream");
                }

                offset += read;
            }

            return buffer;
        }
    }
}
