using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Convai.Scripts.Runtime.Core;
using Convai.Scripts.Runtime.LoggerSystem;
using Convai.Scripts.Runtime.Utils;
using Grpc.Core;
using Service;
using UnityEngine;

namespace Convai.Scripts.Runtime.Features
{
    /// <summary>
    ///     Represents an NPC2NPCGRPCClient that can be used to communicate with the ConvaiService using gRPC.
    /// </summary>
    public class NPC2NPCGRPCClient : MonoBehaviour
    {
        private readonly CancellationTokenSource _cancellationTokenSource = new();
        private string _apiKey;
        private ConvaiService.ConvaiServiceClient _client;
        private NPCGroup _npcGroup;

        public event Action<string, ConvaiGroupNPCController> OnTranscriptAvailable;

        private void OnDestroy()
        {
            _cancellationTokenSource?.Cancel();
            _cancellationTokenSource?.Dispose();
        }

        /// <summary>
        ///     Initializes the NPC2NPCGRPCClient with the given API key and ConvaiService client.
        /// </summary>
        /// <param name="apiKey">The API key to use for authentication.</param>
        /// <param name="client">The ConvaiService client to use for communication.</param>
        public void Initialize(string apiKey, ConvaiService.ConvaiServiceClient client, NPCGroup group)
        {
            _apiKey = apiKey;
            _client = client;
            _npcGroup = group;
        }

        /// <summary>
        ///     Creates an AsyncDuplexStreamingCall with the specified headers.
        /// </summary>
        /// <returns>An AsyncDuplexStreamingCall with the specified headers.</returns>
        private AsyncDuplexStreamingCall<GetResponseRequest, GetResponseResponse> GetAsyncDuplexStreamingCallOptions()
        {
            Metadata headers = new()
            {
                { "source", "Unity" },
                { "version", "3.2.1" }
            };

            CallOptions options = new(headers);
            return _client.GetResponse(options);
        }

        /// <summary>
        ///     Sends the specified user text to the server and receives a response.
        /// </summary>
        /// <param name="userText">The user text to send to the server.</param>
        /// <param name="characterID">The ID of the character to use for the request.</param>
        /// <param name="sessionID">The ID of the session to use for the request.</param>
        /// <param name="isLipSyncActive">Whether lip sync is active for the request.</param>
        /// <param name="faceModel">The face model to use for the request.</param>
        /// <returns>A task that represents the asynchronous operation.</returns>
        public async Task SendTextData(string userText, string characterID, string sessionID, bool isLipSyncActive, FaceModel faceModel, ConvaiGroupNPCController npcController)
        {
            AsyncDuplexStreamingCall<GetResponseRequest, GetResponseResponse> call = GetAsyncDuplexStreamingCallOptions();

            GetResponseRequest getResponseConfigRequest = CreateGetResponseRequest(characterID, sessionID, isLipSyncActive, faceModel, false, null);

            try
            {
                await call.RequestStream.WriteAsync(getResponseConfigRequest);
                await call.RequestStream.WriteAsync(new GetResponseRequest
                {
                    GetResponseData = new GetResponseRequest.Types.GetResponseData
                    {
                        TextData = userText
                    }
                });
                await call.RequestStream.CompleteAsync();

                Task receiveResultsTask = Task.Run(
                    async () => { await ReceiveResultFromServer(call, _cancellationTokenSource.Token, npcController); },
                    _cancellationTokenSource.Token);
                await receiveResultsTask.ConfigureAwait(false);
            }
            catch (Exception)
            {
                // ignored
            }
        }

        /// <summary>
        ///     Creates a GetResponseRequest with the specified parameters.
        /// </summary>
        /// <param name="characterID">The ID of the character to use for the request.</param>
        /// <param name="sessionID">The ID of the session to use for the request.</param>
        /// <param name="isLipSyncActive">Whether lip sync is active for the request.</param>
        /// <param name="faceModel">The face model to use for the request.</param>
        /// <param name="isActionActive">Whether action is active for the request.</param>
        /// <param name="actionConfig">The action configuration to use for the request.</param>
        /// <returns>A GetResponseRequest with the specified parameters.</returns>
        private GetResponseRequest CreateGetResponseRequest(string characterID, string sessionID, bool isLipSyncActive, FaceModel faceModel, bool isActionActive,
            ActionConfig actionConfig)
        {
            GetResponseRequest getResponseConfigRequest = new()
            {
                GetResponseConfig = new GetResponseRequest.Types.GetResponseConfig
                {
                    CharacterId = characterID,
                    ApiKey = _apiKey,
                    SessionId = sessionID,
                    AudioConfig = new AudioConfig
                    {
                        EnableFacialData = isLipSyncActive,
                        FaceModel = faceModel
                    }
                }
            };

            if (isActionActive)
                getResponseConfigRequest.GetResponseConfig.ActionConfig = actionConfig;

            return getResponseConfigRequest;
        }

        /// <summary>
        ///     Receives a response from the server asynchronously.
        /// </summary>
        /// <param name="call">The AsyncDuplexStreamingCall to use for receiving the response.</param>
        /// <param name="cancellationToken">The cancellation token to use for cancelling the operation.</param>
        /// <returns>A task that represents the asynchronous operation.</returns>
        private async Task ReceiveResultFromServer(AsyncDuplexStreamingCall<GetResponseRequest, GetResponseResponse> call, CancellationToken cancellationToken, ConvaiGroupNPCController npcController)
        {
            ConvaiLogger.Info("Receiving response from server", ConvaiLogger.LogCategory.Character);
            Queue<LipSyncBlendFrameData> lipSyncBlendFrameQueue = new();
            ConvaiNPC convaiNPC = npcController.ConvaiNPC;
            bool firstSilFound = false;
            while (!cancellationToken.IsCancellationRequested && await call.ResponseStream.MoveNext(cancellationToken).ConfigureAwait(false))
                try
                {
                    GetResponseResponse result = call.ResponseStream.Current;
                    // Process the received response here
                    if (result.AudioResponse != null)
                        if (result.AudioResponse.AudioData != null)
                        {
                            MainThreadDispatcher.Instance.RunOnMainThread(() => OnTranscriptAvailable?.Invoke(result.AudioResponse.TextData, npcController));
                            if (result.AudioResponse.AudioData.ToByteArray().Length > 46)
                            {
                                byte[] wavBytes = result.AudioResponse.AudioData.ToByteArray();

                                // will only work for wav files
                                if (convaiNPC.convaiLipSync == null)
                                {
                                    ConvaiLogger.DebugLog($"Enqueuing responses: {result.AudioResponse.TextData}", ConvaiLogger.LogCategory.LipSync);
                                    convaiNPC.EnqueueResponse(result);
                                }
                                else
                                {
                                    LipSyncBlendFrameData.FrameType frameType =
                                        convaiNPC.convaiLipSync.faceModel == FaceModel.OvrModelName
                                            ? LipSyncBlendFrameData.FrameType.Visemes
                                            : LipSyncBlendFrameData.FrameType.Blendshape;
                                    lipSyncBlendFrameQueue.Enqueue(
                                        new LipSyncBlendFrameData(
                                            (int)(WavUtility.CalculateDurationSeconds(wavBytes) * 30),
                                            result,
                                            frameType
                                        )
                                    );
                                }
                            }

                            if (result.AudioResponse.VisemesData != null)
                                if (convaiNPC.convaiLipSync != null)
                                {
                                    //ConvaiLogger.Info(result.AudioResponse.VisemesData, ConvaiLogger.LogCategory.LipSync);
                                    if (result.AudioResponse.VisemesData.Visemes.Sil == -2 || result.AudioResponse.EndOfResponse)
                                    {
                                        if (firstSilFound) lipSyncBlendFrameQueue.Dequeue().Process(convaiNPC);
                                        firstSilFound = true;
                                    }
                                    else
                                    {
                                        lipSyncBlendFrameQueue.Peek().Enqueue(result.AudioResponse.VisemesData);
                                    }
                                }

                            if (result.AudioResponse.BlendshapesData != null)
                                if (convaiNPC.convaiLipSync != null)
                                {
                                    if (lipSyncBlendFrameQueue.Peek().CanProcess() || result.AudioResponse.EndOfResponse)
                                    {
                                        lipSyncBlendFrameQueue.Dequeue().Process(convaiNPC);
                                    }
                                    else
                                    {
                                        lipSyncBlendFrameQueue.Peek().Enqueue(result.AudioResponse.FaceEmotion.ArKitBlendShapes);

                                        if (lipSyncBlendFrameQueue.Peek().CanPartiallyProcess()) lipSyncBlendFrameQueue.Peek().ProcessPartially(convaiNPC);
                                    }
                                }

                            if (result.AudioResponse.EndOfResponse)
                                MainThreadDispatcher.Instance.RunOnMainThread(npcController.EndOfResponseReceived);
                        }
                }
                catch (RpcException rpcException)
                {
                    if (rpcException.StatusCode == StatusCode.Cancelled)
                        ConvaiLogger.Error(rpcException, ConvaiLogger.LogCategory.Character);
                    else
                        throw;
                }
                catch (Exception ex)
                {
                    ConvaiLogger.DebugLog(ex, ConvaiLogger.LogCategory.Character);
                }
        }
    }
}