using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Timers;
using common;
using Grpc.Core;
using Net.Eagle0.Eagle.Api;
using static Net.Eagle0.Eagle.Api.UpdateStreamRequest.Types;
using GameUpdate = Net.Eagle0.Eagle.Api.GameUpdate;
using Timer = System.Timers.Timer;

namespace eagle {
    using EagleGameId = Int64;
    using ShardokGameId = String;
    using ProvinceId = Int32;

    public interface ILobbySubscriber {
        void ReceiveLobbyUpdate(LobbyResponse lobbyResponse);
        void ReceivePregeneratedTextUpdate(PregeneratedTextResponse response);
        void HandleJoinGameResponse(JoinGameResponse response);
        void HandleCreateGameResponse(CreateGameResponse response);
        void HandleCustomBattleResponse(CustomBattleResponse response);
    }

    public class PersistentClientConnection : IDisposable {
        private const double TimeoutSeconds = 300.0;
        private const double HeartbeatTimerSeconds = 10.0;

        private DateTime _lastResponseReceived = DateTime.UtcNow;
        private readonly Metadata _credentials;
        private readonly CancellationToken _cancellationToken;

        private readonly Logger _remoteEagleClientLogger = Logger.GetLogger("ConnectionLogger");
        private readonly Logger _timingsLogger = Logger.GetLogger("ConnectionLogger");
        private Timer _timer = null;

        private DateTime? GetDeadlineFromNow() {
            return DateTime.UtcNow.AddSeconds(TimeoutSeconds);
        }

        private readonly Eagle.EagleClient _grpcClient;
        private AsyncDuplexStreamingCall<UpdateStreamRequest, UpdateStreamResponse> _streamingCall;

        private Dictionary<EagleGameId, IClientConnectionSubscriber> _subscribers = new();
        private ILobbySubscriber _lobbySubscriber = null;
        private Thread _streamingCallThread;
        private CancellationTokenSource _threadCancellationTokenSource;
        private CancellationToken _currentThreadToken;

        private List<PostCommandRequest> _pendingCommands = new();

        public PersistentClientConnection(
                Eagle.EagleClient grpcClient,
                Metadata credentials,
                CancellationToken cancellationToken) {
            _grpcClient = grpcClient;
            _credentials = credentials;
            _cancellationToken = cancellationToken;
        }

        private void StreamOneGame(IClientConnectionSubscriber subscriber) {
            var streamGameRequest = new StreamGameRequest {
                GameId = subscriber.GameId,
                UnfilteredResultCount = subscriber.LastUnfilteredResultCount
            };

            streamGameRequest.ShardokViewStatuses.AddRange(subscriber.ShardokViewStatuses.Select(
                    status => new StreamGameRequest.Types.ShardokViewStatus {
                        FilteredResultCount = status.filteredResultCount,
                        ShardokGameId = status.shardokGameId
                    }));
            streamGameRequest.StreamingTextStatuses.AddRange(
                    subscriber.StreamingTextStatuses.Select(
                            status => new StreamGameRequest.Types.StreamingTextStatus {
                                LlmIdentifier = status.llmIdentifier,
                                KnownByteCount = status.knownByteCount,
                                KnownComplete = status.knownComplete
                            }));

            var request = new UpdateStreamRequest { StreamGameRequest = streamGameRequest };

            DoWithStreamingCall(async (sc) => {
                await sc.RequestStream.WriteAsync(request);
                return true;
            });
        }

        public async Task Connect() {
            _remoteEagleClientLogger.LogLine($"Connect() called");

            // Dispose existing streaming call before creating new one
            lock (this) {
                _streamingCall?.Dispose();
                _streamingCall = null;

                // Cancel and dispose existing thread cancellation token
                _threadCancellationTokenSource?.Cancel();
                _threadCancellationTokenSource?.Dispose();
                _threadCancellationTokenSource = new CancellationTokenSource();
                _currentThreadToken = _threadCancellationTokenSource.Token;
            }

            var call = _grpcClient.StreamUpdates(
                    headers: _credentials,
                    deadline: GetDeadlineFromNow(),
                    cancellationToken: _cancellationToken);
            Queue<PostCommandRequest> pendingCommands = new Queue<PostCommandRequest>();
            try {
                lock (this) {
                    _streamingCall = call;
                    _streamingCallThread = new Thread(HandleStreamingCall);
                    _streamingCallThread.Start();

                    foreach (var kv in _subscribers) { StreamOneGame(kv.Value); }
                }

                await TryPendingCommands();
            } catch (Exception e) {
                _remoteEagleClientLogger.LogLine($"Got exception {e} in Connect()");
                lock (this) {
                    while (pendingCommands.Any()) {
                        _pendingCommands.Add(pendingCommands.Dequeue());
                    }
                }
            }
        }

        private async Task<bool> TryPendingCommands() {
            Queue<PostCommandRequest> pendingCommands = new();
            try {
                lock (this) {
                    foreach (var cmd in _pendingCommands) { pendingCommands.Enqueue(cmd); }

                    _pendingCommands = new();
                }
                if (pendingCommands.Any()) {
                    _remoteEagleClientLogger.LogLine($"preparing to post pending commands");

                    while (pendingCommands.Any()) {
                        var nextCommand = pendingCommands.Dequeue();
                        if (_subscribers.TryGetValue(nextCommand.GameId, out var subscriber)) {
                            switch (nextCommand.Command.SealedValueCase) {
                                case UniversalCommand.SealedValueOneofCase.EagleCommand:
                                    Int64 providedToken =
                                            nextCommand.Command.EagleCommand.EagleToken;
                                    if (subscriber.CurrentEagleToken is Int64 eagleToken) {
                                        if (eagleToken == providedToken) {
                                            _remoteEagleClientLogger.LogLine(
                                                    "Found a matching pending command with token " +
                                                    eagleToken);
                                            await PostRequest(nextCommand);
                                        } else if (eagleToken > providedToken) {
                                            _remoteEagleClientLogger.LogLine(
                                                    $"{providedToken} seems to be stale, dropping");
                                        } else {
                                            _remoteEagleClientLogger.LogLine(
                                                    $"{providedToken} seems to be from the future, adding back to the queue");
                                            lock (this) { _pendingCommands.Add(nextCommand); }
                                        }
                                    } else {
                                        _remoteEagleClientLogger.LogLine(
                                                "No matching command for token " + providedToken);
                                    }

                                    break;
                                case UniversalCommand.SealedValueOneofCase.ShardokCommand: {
                                    Int64 providedShardokToken =
                                            nextCommand.Command.ShardokCommand.ShardokToken;
                                    if (subscriber.CurrentShardokToken(
                                                nextCommand.Command.ShardokCommand.GameId)
                                                is Int64 shardokToken &&
                                        shardokToken == providedShardokToken) {
                                        _remoteEagleClientLogger.LogLine(
                                                "Found a matching pending command with token " +
                                                shardokToken);
                                        await PostRequest(nextCommand);
                                    } else {
                                        _remoteEagleClientLogger.LogLine(
                                                "No matching command for token " +
                                                providedShardokToken);
                                    }

                                    break;
                                }
                                case UniversalCommand.SealedValueOneofCase
                                        .ShardokPlacementCommands: {
                                    Int64 providedShardokToken =
                                            nextCommand.Command.ShardokPlacementCommands
                                                    .ShardokToken;
                                    if (subscriber.CurrentShardokToken(
                                                nextCommand.Command.ShardokCommand.GameId)
                                                is Int64 shardokToken &&
                                        shardokToken == providedShardokToken) {
                                        _remoteEagleClientLogger.LogLine(
                                                "Found a matching pending command with token " +
                                                shardokToken);
                                        await PostRequest(nextCommand);
                                    } else {
                                        _remoteEagleClientLogger.LogLine(
                                                "No matching command for token " +
                                                providedShardokToken);
                                    }

                                    break;
                                }
                            }

                            await PostRequest(nextCommand);
                        }
                    }

                    _remoteEagleClientLogger.LogLine($"pendingCommands completed");
                }

                return true;
            } catch (Exception e) {
                _remoteEagleClientLogger.LogLine($"Got exception {e} in TryPendingCommands()");
                lock (this) {
                    while (pendingCommands.Any()) {
                        _pendingCommands.Add(pendingCommands.Dequeue());
                    }
                }

                return false;
            }
        }

        public void Disconnect() {
            lock (this) {
                _streamingCall?.Dispose();
                _streamingCall = null;
            }
        }

        public void Dispose() {
            lock (this) {
                // Dispose timers first to stop any pending callbacks
                if (_timer != null) {
                    _timer.Enabled = false;
                    _timer.Dispose();
                    _timer = null;
                }

                if (_retryTimer != null) {
                    _retryTimer.Enabled = false;
                    _retryTimer.Dispose();
                    _retryTimer = null;
                }

                // Dispose streaming call
                _streamingCall?.Dispose();
                _streamingCall = null;

                // Clear subscribers
                _subscribers.Clear();
                _lobbySubscriber = null;

                // Clear pending commands
                _pendingCommands.Clear();

                // Cancel and dispose thread cancellation token
                _threadCancellationTokenSource?.Cancel();

                // Give thread a chance to exit gracefully
                if (_streamingCallThread != null) {
                    if (!_streamingCallThread.Join(TimeSpan.FromSeconds(2))) {
                        Console.WriteLine("Warning: Streaming call thread did not exit gracefully");
                    }
                    _streamingCallThread = null;
                }

                _threadCancellationTokenSource?.Dispose();
                _threadCancellationTokenSource = null;
            }
        }

        public void SetLobbySubscriber(ILobbySubscriber subscriber) {
            _lobbySubscriber = subscriber;
        }

        public void Subscribe(IClientConnectionSubscriber subscriber) {
            lock (this) {
                _subscribers[subscriber.GameId] = subscriber;
                StreamOneGame(subscriber);
            }
        }

        public async Task PostError(EagleGameId gameId, string errorMessage, string stackTrace) {
            await DoWithStreamingCall(async (streamingCall) => {
                await streamingCall.RequestStream.WriteAsync(new UpdateStreamRequest {
                    ErrorRequest =
                            new ErrorRequest {
                                ErrorMessage = errorMessage,
                                GameId = gameId,
                                StackTrace = stackTrace
                            }
                });
                return true;
            });
        }

        public void Unsubscribe(IClientConnectionSubscriber subscriber) {
            lock (this) { _subscribers.Remove(subscriber.GameId); }
        }

        private async Task PostRequest(PostCommandRequest request) {
            try {
                lock (this) { _pendingCommands.Add(request); }

                await DoWithStreamingCall(async (streamingCall) => {
                    await streamingCall.RequestStream.WriteAsync(
                            new UpdateStreamRequest { PostCommandRequest = request });
                    return true;
                });
                lock (this) { _pendingCommands.Remove(request); }
            } catch (Exception e) { Console.WriteLine("Failed to post command: " + e); }
        }

        private async Task
        PostUniversalCommand(EagleGameId gameId, UniversalCommand universalCommand) {
            var request = new PostCommandRequest { GameId = gameId, Command = universalCommand };

            await PostRequest(request);
        }

        public async Task PostEagleCommand(
                EagleGameId gameId,
                Int64 token,
                ProvinceId provinceId,
                SelectedCommand command) {
            var startTime = DateTime.UtcNow;

            await PostUniversalCommand(
                    gameId,
                    new UniversalCommand {
                        EagleCommand =
                                new EagleCommand {
                                    ProvinceId = provinceId,
                                    Command = command,
                                    EagleToken = token
                                }
                    });
            _timingsLogger.LogLine($"POST    {(DateTime.UtcNow - startTime).TotalMilliseconds} ms");
        }

        public async Task PostShardokCommand(
                EagleGameId eagleGameId,
                ShardokGameId shardokGameId,
                Int32 playerId,
                Int64 token,
                Int32 index,
                Int32? roll) {
            var startTime = DateTime.UtcNow;

            await PostUniversalCommand(
                    gameId: eagleGameId,
                    new UniversalCommand {
                        ShardokCommand =
                                new ShardokCommand {
                                    GameId = shardokGameId,
                                    PlayerId = playerId,
                                    ShardokToken = token,
                                    Index = index,
                                    Roll = roll
                                }
                    });

            _timingsLogger.LogLine(
                    $"PostShardokCommand took {(DateTime.UtcNow - startTime).TotalMilliseconds} ms");
        }

        public async Task PostShardokPlacementCommands(
                EagleGameId eagleGameId,
                ShardokGameId shardokGameId,
                Int32 playerId,
                List<ShardokPlacementCommands.Types.ShardokPlacementCommand> placementCommands,
                Int64 token) {
            var sc = new ShardokPlacementCommands {
                GameId = shardokGameId,
                PlayerId = playerId,
                ShardokToken = token
            };

            sc.PlacementCommands.AddRange(placementCommands);

            await PostUniversalCommand(
                    eagleGameId,
                    new UniversalCommand { ShardokPlacementCommands = sc });
        }

        private delegate Task<bool> WithStreamingCallDelegate(
                AsyncDuplexStreamingCall<UpdateStreamRequest, UpdateStreamResponse> call);
        private async Task<bool> DoWithStreamingCall(WithStreamingCallDelegate action) {
            try {
                return await action(_streamingCall);
            } catch (RpcException e) {
                if (e.StatusCode == StatusCode.Cancelled) {
                    // This is expected when the connection is closed.
                    return false;
                } else {
                    throw;
                }
            }
        }

        public async Task<bool> SendUpdateStreamRequestAsync(UpdateStreamRequest request) {
            AsyncDuplexStreamingCall<UpdateStreamRequest, UpdateStreamResponse> sc = null;
            lock (this) {
                sc = _streamingCall;
                if (sc == null) return false;
            }

            try {
                await sc.RequestStream.WriteAsync(request, _cancellationToken);
                return true;
            } catch (RpcException e) {
                if (e.StatusCode == StatusCode.Cancelled) {
                    // This is expected when the connection is closed.
                    return false;
                } else {
                    throw;
                }
            }
        }

        public delegate Task<TResponseType> WithGrpcClientDelegateAsync<TResponseType>(
                Eagle.EagleClient client);

        private Timer _retryTimer = null;

        private void HandleGameUpdate(GameUpdate gameUpdate, DateTime receivedTime) {
            switch (gameUpdate.GameUpdateDetailsCase) {
                case GameUpdate.GameUpdateDetailsOneofCase.ErrorResponse:
                    _remoteEagleClientLogger.LogLine("Got an error response!");
                    MainQueue.Q.Enqueue(() => {
                        lock (this) { _subscribers.Remove(gameUpdate.GameId); }
                    });
                    break;

                case GameUpdate.GameUpdateDetailsOneofCase.ActionResultResponse:
                case GameUpdate.GameUpdateDetailsOneofCase.ShardokActionResultResponse:
                case GameUpdate.GameUpdateDetailsOneofCase.StreamingTextResponse:
                    MainQueue.Q.Enqueue(async () => {
                        IClientConnectionSubscriber subscriber;
                        lock (this) {
                            if (_subscribers.TryGetValue(gameUpdate.GameId, out subscriber)) {
                                subscriber.ReceiveGameUpdate(gameUpdate);
                            } else {
                                _subscribers.Remove(gameUpdate.GameId);
                            }
                        }

                        await TryPendingCommands();

                        var processTime = (DateTime.UtcNow - receivedTime).TotalMilliseconds;
                        if (processTime > 100.0) {
                            _timingsLogger.LogLine($"PROCESS {processTime} ms");
                        }
                    });
                    break;
            }
        }

        private async void HandleStreamingCall() {
            try {
                AsyncDuplexStreamingCall<UpdateStreamRequest, UpdateStreamResponse> sc;
                CancellationToken threadToken;
                lock (this) {
                    sc = _streamingCall;
                    threadToken = _currentThreadToken;
                }

                SetUpTimer();

                var waitStartTime = DateTime.UtcNow;
                while (sc != null && !_currentThreadToken.IsCancellationRequested &&
                       await sc.ResponseStream.MoveNext(_cancellationToken)) {
                    var receivedTime = DateTime.UtcNow;
                    _lastResponseReceived = receivedTime;
                    SetUpTimer();
                    var waitTime = (receivedTime - waitStartTime).TotalMilliseconds;
                    if (waitTime > 100.0) { _timingsLogger.LogLine($"WAIT    {waitTime} ms"); }

                    lock (this) {
                        var current = sc.ResponseStream.Current;

                        switch (current.ResponseDetailsCase) {
                            case UpdateStreamResponse.ResponseDetailsOneofCase.HeartbeatResponse:
                                _remoteEagleClientLogger.LogLine("Got a heartbeat response!");
                                break;

                            case UpdateStreamResponse.ResponseDetailsOneofCase.GameUpdate:
                                HandleGameUpdate(current.GameUpdate, receivedTime);
                                break;

                            case UpdateStreamResponse.ResponseDetailsOneofCase.LobbyResponse:
                                if (_lobbySubscriber != null) {
                                    MainQueue.Q.Enqueue(() => {
                                        _lobbySubscriber.ReceiveLobbyUpdate(current.LobbyResponse);
                                    });
                                }

                                break;

                            case UpdateStreamResponse.ResponseDetailsOneofCase
                                    .PregeneratedTextResponse:
                                if (_lobbySubscriber != null) {
                                    MainQueue.Q.Enqueue(() => {
                                        _lobbySubscriber.ReceivePregeneratedTextUpdate(
                                                current.PregeneratedTextResponse);
                                    });
                                }

                                break;

                            case UpdateStreamResponse.ResponseDetailsOneofCase.JoinGameResponse:
                                if (_lobbySubscriber != null) {
                                    MainQueue.Q.Enqueue(() => {
                                        _lobbySubscriber.HandleJoinGameResponse(
                                                current.JoinGameResponse);
                                    });
                                }

                                break;

                            case UpdateStreamResponse.ResponseDetailsOneofCase.CreateGameResponse:
                                if (_lobbySubscriber != null) {
                                    MainQueue.Q.Enqueue(() => {
                                        _lobbySubscriber.HandleCreateGameResponse(
                                                current.CreateGameResponse);
                                    });
                                }

                                break;
                            case UpdateStreamResponse.ResponseDetailsOneofCase.CustomBattleResponse:
                                if (_lobbySubscriber != null) {
                                    MainQueue.Q.Enqueue(() => {
                                        _lobbySubscriber.HandleCustomBattleResponse(
                                                current.CustomBattleResponse);
                                    });
                                }

                                break;

                            case UpdateStreamResponse.ResponseDetailsOneofCase.HexMapResponse:
                                var hexMapResponse = current.HexMapResponse;
                                foreach (var mapInfo in hexMapResponse.MapInfos) {
                                    HexMapStorage.Instance.Save(
                                            mapInfo.MapName,
                                            mapInfo.MapHash,
                                            mapInfo.Map);
                                }

                                break;
                        }

                        sc = _streamingCall;
                    }

                    waitStartTime = DateTime.UtcNow;
                }

                _remoteEagleClientLogger.LogLine(
                        "How did we get here? This is not my beautiful wife!");
            } catch (RpcException e) {
                lock (this) {
                    switch (e.StatusCode) {
                        case StatusCode.Cancelled:
                            _remoteEagleClientLogger.LogLine($"Got exception {e}");
                            if (_cancellationToken.IsCancellationRequested ||
                                _currentThreadToken.IsCancellationRequested) {
                                return;
                            }

                            Connect();

                            break;

                        case StatusCode.Internal:
                            _remoteEagleClientLogger.LogLine($"Got exception {e}");
                            Connect();
                            break;

                        case StatusCode.DeadlineExceeded:
                            _remoteEagleClientLogger.LogLine("Got DeadlineExceeded");
                            Connect();
                            break;

                        case StatusCode.Unavailable:
                            _remoteEagleClientLogger.LogLine("Got Unavailable");
                            _retryTimer?.Dispose();
                            _retryTimer = new Timer();
                            _retryTimer.AutoReset = false;
                            _retryTimer.Interval = 5000;
                            _retryTimer.Elapsed += (sender, args) => Connect();
                            _retryTimer.Enabled = true;
                            break;

                        default:
                            _remoteEagleClientLogger.LogLine(
                                    $"Something went very wrong, caught exception {e}");
                            break;
                    }
                }
            } catch (ObjectDisposedException e) {
                _remoteEagleClientLogger.LogLine($"Got ObjectDisposedException {e}");
                Connect();
            }

            return;
        }

        private void SetUpTimer() {
            if (_timer != null) {
                _timer.Enabled = false;
                _timer.Dispose();
                _timer = null;
            }

            _timer = new Timer { AutoReset = false, Interval = HeartbeatTimerSeconds * 1000 };
            _timer.Elapsed += TimerFired;
            _timer.Enabled = true;
        }

        private void SendHeartbeatRequest(
                AsyncDuplexStreamingCall<UpdateStreamRequest, UpdateStreamResponse> sc) {
            var heartbeatRequest = new UpdateStreamRequest {
                HeartbeatRequest =
                        new HeartbeatRequest {
                            ClientTimestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds()
                        }
            };
            sc.RequestStream.WriteAsync(heartbeatRequest);
        }

        private void TimerFired(object sender, ElapsedEventArgs e) {
            if (sender != _timer) {
                // This timer was canceled; ignore
                return;
            }
            if (_cancellationToken.IsCancellationRequested ||
                _currentThreadToken.IsCancellationRequested) {
                return;
            }

            AsyncDuplexStreamingCall<UpdateStreamRequest, UpdateStreamResponse> sc;
            lock (this) { sc = _streamingCall; }

            if (sc == null) {
                // Already canceled
                return;
            }

            _timer.Enabled = false;
            _timer.Dispose();
            _timer = null;

            var now = DateTime.UtcNow;
            var lastResponseReceived = _lastResponseReceived;

            var secondsSinceResponse = (now - lastResponseReceived).TotalSeconds;
            if (secondsSinceResponse > 2.0 * HeartbeatTimerSeconds) {
                var toCancel = _streamingCall;

                if (toCancel != null) {
                    toCancel.Dispose();
                    lock (this) {
                        _streamingCall = null;
                        Connect();
                    }
                }
            } else if (secondsSinceResponse > HeartbeatTimerSeconds) {
                SendHeartbeatRequest(sc);
            }
        }
    }
}