#nullable enable
using IGP.UnitySDK.Models;
using IGP.UnitySDK;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipes;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

using IGP.Multiplayer.Core;
using UnityEngine;

namespace IGP.Multiplayer
{
    /// <summary>
    /// Long-lived Windows named pipe client used for hosted IGP control-plane
    /// operations and room snapshot delivery.
    /// </summary>
    internal sealed class IGPHostSessionClient : IDisposable
    {
        private const string LogScope = "host-session";
        private const int ConnectTimeoutMs = 5000;
        private const int DefaultCommandTimeoutMs = 30000;
        private const int MaxFramePayloadBytes = 8 * 1024 * 1024;

        private readonly object syncRoot = new object();
        private readonly string pipeEndpoint;
        private readonly string secret;
        private readonly int commandTimeoutMs;
        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 IGPLogLevel LogLevel { get; set; }

        internal IGPHostSessionClient(string pipeEndpoint, string secret)
            : this(pipeEndpoint, secret, DefaultCommandTimeoutMs)
        {
        }

        internal IGPHostSessionClient(string pipeEndpoint, string secret, int commandTimeoutMs)
        {
            this.pipeEndpoint = pipeEndpoint ?? string.Empty;
            this.secret = secret ?? string.Empty;
            this.commandTimeoutMs = commandTimeoutMs > 0
                ? commandTimeoutMs
                : DefaultCommandTimeoutMs;
        }

        internal bool IsAttached { get; private set; }

        internal event Action<IGPHostSessionSnapshotEvent>? RoomSnapshotReceived;
        internal event Action<IGPHostSessionRoomEvent>? RoomEventReceived;
        internal event Action<string>? Detached;
        internal event Action<IGPMultiplayerErrorEvent>? 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("connect-replace");

            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
            {
                LogInfo($"event=connect pipe={pipeName} room={roomId} player={playerId}");
                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);
                LogInfo($"event=attach-request bytes={attachPayload.Length}");
                await WriteFrameAsync(attachPayload, cancellationToken);

                var attachWait = attachCompletionSource;
                using (var timeoutCts = new CancellationTokenSource())
                using (cancellationToken.Register(() => attachWait.TrySetCanceled(cancellationToken)))
                using (timeoutCts.Token.Register(() => attachWait.TrySetException(CreateAttachTimeoutException())))
                {
                    timeoutCts.CancelAfter(commandTimeoutMs);
                    await attachWait.Task;
                }
            }
            catch
            {
                await DisconnectAsync("connect-failed");
                throw;
            }
        }

        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> 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<IGPHostSessionDataPlaneResponse> RequestDataPlaneAsync(
            CancellationToken cancellationToken = default)
        {
            if (pipe == null || !IsAttached)
            {
                throw new IGPSDKException("IGP host session is not attached");
            }

            var requestId = Guid.NewGuid().ToString("N");
            LogInfo($"event=data-plane-request requestId={requestId}");
            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 (var timeoutCts = new CancellationTokenSource())
                using (cancellationToken.Register(() => completionSource.TrySetCanceled(cancellationToken)))
                using (timeoutCts.Token.Register(() => completionSource.TrySetException(CreateDataPlaneTimeoutException())))
                {
                    timeoutCts.CancelAfter(commandTimeoutMs);
                    var result = await completionSource.Task;
                    if (!result.Success)
                    {
                        LogWarning($"event=data-plane-result requestId={requestId} success=false message={result.Message}");
                        throw new IGPSDKException(string.IsNullOrWhiteSpace(result.Message)
                            ? "Failed to request data plane descriptor from hosted session"
                            : result.Message);
                    }

                    LogInfo(
                        $"event=data-plane-result requestId={requestId} success=true mode={result.DataPlane?.Mode} " +
                        $"target={result.DataPlane?.Host}:{result.DataPlane?.Port}");
                    return result;
                }
            }
            catch
            {
                lock (syncRoot)
                {
                    pendingDataPlaneRequests.Remove(requestId);
                }

                throw;
            }
        }

        public async Task DisconnectAsync(string reason = "explicit")
        {
            var localPipe = pipe;
            var localLifetimeCts = lifetimeCts;
            var localReadLoopTask = readLoopTask;
            bool hadSession = localPipe != null || localLifetimeCts != null || localReadLoopTask != null || IsAttached;
            pipe = null;
            lifetimeCts = null;
            readLoopTask = null;
            IsAttached = false;
            attachedRoomId = string.Empty;
            attachedPlayerId = string.Empty;
            attachAcknowledged = false;
            initialSnapshotReceived = false;
            if (hadSession)
            {
                LogInfo($"event=disconnect-start reason={IGPLog.FormatValue(reason)}");
            }
            else
            {
                LogDebug($"event=disconnect-skipped reason=no-session caller={IGPLog.FormatValue(reason)}");
            }

            if (localLifetimeCts != null)
            {
                try
                {
                    localLifetimeCts.Cancel();
                }
                catch
                {
                    // Ignore cancellation failures during shutdown.
                }
            }

            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 (localReadLoopTask != null)
            {
                try
                {
                    await localReadLoopTask;
                }
                catch
                {
                    // Read loop exceptions are surfaced through events/TCS.
                }
            }

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

            if (hadSession)
            {
                LogInfo($"event=disconnect-complete reason={IGPLog.FormatValue(reason)}");
            }
        }

        public void Dispose()
        {
            _ = DisconnectAsync("dispose").ContinueWith(_ =>
            {
                try
                {
                    writeLock.Dispose();
                }
                catch
                {
                    // Ignore dispose failures during shutdown.
                }
            }, TaskScheduler.Default);
        }

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

            var requestId = Guid.NewGuid().ToString("N");
            LogDebug($"event=command-send command={command} requestId={requestId}");
            var completionSource = new TaskCompletionSource<IGPHostSessionCommandResult>(
                TaskCreationOptions.RunContinuationsAsynchronously);

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

            try
            {
                var payload = IGPHostSessionProtocol.EncodeCommandRequest(
                    requestId,
                    command,
                    boolValue,
                    stringValue,
                    contentJson);
                await WriteFrameAsync(payload, cancellationToken);

                using (var timeoutCts = new CancellationTokenSource())
                using (cancellationToken.Register(() => completionSource.TrySetCanceled(cancellationToken)))
                using (timeoutCts.Token.Register(() => completionSource.TrySetException(CreateCommandTimeoutException(command))))
                {
                    timeoutCts.CancelAfter(commandTimeoutMs);
                    var result = await completionSource.Task;
                    if (!result.Success)
                    {
                        LogWarning($"event=command-result command={command} requestId={requestId} success=false message={result.Message}");
                        throw new IGPSDKException(string.IsNullOrWhiteSpace(result.Message)
                            ? $"Host session command failed: {command}"
                            : result.Message);
                    }

                    LogDebug($"event=command-result command={command} requestId={requestId} success=true");
                    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 (ObjectDisposedException) when (cancellationToken.IsCancellationRequested)
            {
                // Expected when the pipe is disposed during shutdown.
            }
            catch (IOException) when (cancellationToken.IsCancellationRequested)
            {
                // Expected when the pipe is disposed during shutdown.
            }
            catch (Exception ex)
            {
                attachCompletionSource?.TrySetException(ex);
                FailPendingRequests(ex);
                FailPendingDataPlaneRequests(ex);
                LogWarning($"event=read-loop-failed error={ex.Message}");
                ErrorOccurred?.Invoke(new IGPMultiplayerErrorEvent(
                    "SESSION_BRIDGE_ERROR",
                    ex.Message,
                    "hosted-session",
                    null,
                    true,
                    ex));
            }
            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.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";
                    var error = new IGPSDKException(errorMessage, code);
                    attachCompletionSource?.TrySetException(error);
                    FailPendingRequests(error);
                    FailPendingDataPlaneRequests(error);
                    ErrorOccurred?.Invoke(new IGPMultiplayerErrorEvent(
                        code,
                        errorMessage,
                        "hosted-session",
                        null,
                        false,
                        message.Error));
                    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 IGPSDKException CreateAttachTimeoutException()
        {
            return new IGPSDKException("Host session attach timed out");
        }

        private static IGPSDKException CreateCommandTimeoutException(IGPHostSessionCommandType command)
        {
            return new IGPSDKException($"Host session command timed out: {command}");
        }

        private static IGPSDKException CreateDataPlaneTimeoutException()
        {
            return new IGPSDKException("Host session data-plane request timed out");
        }

        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;
        }

        private void LogDebug(string message)
        {
            IGPLog.Debug(LogScope, "client", message);
        }

        private void LogInfo(string message)
        {
            IGPLog.Info(LogScope, "client", message);
        }

        private void LogWarning(string message)
        {
            IGPLog.Warning(LogScope, "client", message);
        }
    }
}
