using System.Security.Cryptography;
using Azure;
using Azure.Communication;
using Azure.Communication.CallAutomation;
using Azure.Communication.Identity;
using Azure.Communication.Rooms;
using Coravel.Queuing.Interfaces;
using EnotaryoPH.Data;
using EnotaryoPH.Data.Entities;
using EnotaryoPH.Web.Common.Jobs;
using EnotaryoPH.Web.Common.Jobs.Models;

namespace EnotaryoPH.Web.Common.Services
{
    public class VideoConferenceService : IVideoConferenceService
    {
        private const int VideoConferenceExpirationInHours = 2;

        private readonly CallAutomationClient _callAutomationClient;
        private readonly CommunicationIdentityClient _communicationIdentityClient;
        private readonly IConfiguration _configuration;
        private readonly NotaryoDBContext _dbContext;
        private readonly IEventService _eventService;
        private readonly IPasswordService _passwordService;
        private readonly IQueue _queue;
        private readonly RoomsClient _roomsClient;

        public VideoConferenceService(NotaryoDBContext dbContext, CommunicationIdentityClient communicationIdentityClient, RoomsClient roomsClient, CallAutomationClient callAutomationClient, IConfiguration configuration, IEventService eventService, IPasswordService passwordService, IQueue queue)
        {
            _dbContext = dbContext;
            _communicationIdentityClient = communicationIdentityClient;
            _roomsClient = roomsClient;
            _callAutomationClient = callAutomationClient;
            _configuration = configuration;
            _eventService = eventService;
            _passwordService = passwordService;
            _queue = queue;
        }

        public async Task ApproveTransactionAsync(Guid transaction_UID)
        {
            var transaction = _dbContext.Transactions
                .Include(t => t.TransactionSignatories)
                .ThenInclude(ts => ts.User)
                .Include(t => t.Schedule)
                .Include(t => t.Lawyer)
                .ThenInclude(l => l.User)
                .Include(t => t.Principal)
                .FirstOrDefault(t => t.Transaction_UID == transaction_UID);
            NoDataException.ThrowIfNull(transaction, nameof(Transaction), transaction_UID);
            transaction.Status = nameof(TransactionState.Approved);
            transaction.TransactionSignatories.ForEach(ts => ts.Status = nameof(SignatoryStatus.Approved));
            transaction.Schedule.Status = nameof(VideoConferenceStatus.Completed);
            _dbContext.Update(transaction);
            _dbContext.SaveChanges();

            await Task.WhenAll(
                _eventService.LogAsync(NotaryoEvent.TransactionApproved, transaction_UID),
                StopRecordingAsync(transaction.Schedule)
            );
        }

        public bool CanStart(Guid transaction_UID)
        {
            if (transaction_UID == Guid.Empty)
            {
                return false;
            }

            var transactionEntity = _dbContext.Transactions
                .AsNoTracking()
                .Include(t => t.TransactionSignatories)
                .FirstOrDefault(t => t.Transaction_UID == transaction_UID);
            if (transactionEntity == null)
            {
                return false;
            }

            var isReadyForVideoCall = transactionEntity.TransactionSignatories.All(signatory => signatory.Status == nameof(SignatoryStatus.FaceMatch));
            var isAcceptedByLawyer = transactionEntity.Status == nameof(TransactionState.Accepted) && transactionEntity.LawyerID > 0;
            return isReadyForVideoCall && isAcceptedByLawyer;
        }

        public Guid GetUIDByTransactionUID(Guid transaction_UID)
        {
            var transactionEntity = _dbContext.Transactions
                .AsNoTracking()
                .FirstOrDefault(t => t.Transaction_UID == transaction_UID);
            if (transactionEntity == null)
            {
                return Guid.Empty;
            }

            var schedule = _dbContext.LawyerVideoConferenceSchedules.AsNoTracking().FirstOrDefault(sched => sched.TransactionID == transactionEntity.TransactionID);
            return schedule == null ? Guid.Empty : schedule.LawyerVideoConferenceSchedule_UID;
        }

        public bool HasExpired(Guid transaction_UID)
        {
            var transactionEntity = _dbContext.Transactions
                .Include(t => t.TransactionSignatories)
                .FirstOrDefault(t => t.Transaction_UID == transaction_UID);
            if (transactionEntity == null)
            {
                return false;
            }
            var schedule = _dbContext.LawyerVideoConferenceSchedules.FirstOrDefault(sched => sched.TransactionID == transactionEntity.TransactionID);
            if (schedule == null)
            {
                return false;
            }
            if (schedule.Status == nameof(VideoConferenceStatus.Expired))
            {
                return true;
            }
            if ((DateTime.UtcNow - schedule.MeetingDate).TotalHours > VideoConferenceExpirationInHours)
            {
                if (!schedule.Status?.IsInList(VideoConferenceStatus.Abandoned, VideoConferenceStatus.Completed) ?? false)
                {
                    schedule.Status = nameof(VideoConferenceStatus.Expired);
                    _dbContext.Update(schedule);
                    _dbContext.SaveChanges();
                }
                return true;
            }
            return false;
        }

        public async Task<Guid> StartAsync(Guid transaction_UID)
        {
            if (!CanStart(transaction_UID))
            {
                throw new ArgumentException("Transaction is not ready for video conference.");
            }
            var schedule = GetOrCreateNewSchedule(transaction_UID);
            if (schedule.Status == nameof(VideoConferenceStatus.New))
            {
                await CreateMeetingRoomAsync(schedule);
                await CreateAndEmailOTPAsync(schedule);
                _dbContext.UpdateOrCreate(schedule);
                _dbContext.SaveChanges();
            }
            return schedule.LawyerVideoConferenceSchedule_UID;
        }

        public async Task StartRecordingAsync(Guid transaction_UID, string serverCallID)
        {
            var transactionEntity = _dbContext.Transactions
                .Include(t => t.TransactionSignatories)
                .Single(t => t.Transaction_UID == transaction_UID);
            NoDataException.ThrowIfNull(transactionEntity, nameof(Transaction), transaction_UID);

            var schedule = _dbContext.LawyerVideoConferenceSchedules
                .FirstOrDefault(sched => sched.TransactionID == transactionEntity.TransactionID);
            NoDataException.ThrowIfNull(schedule, nameof(LawyerVideoConferenceSchedule), transactionEntity.TransactionID, FullName.Of(transactionEntity.TransactionID));

            if (string.IsNullOrEmpty(schedule.RecordingID))
            {
                schedule.RecordingID = await StartRecordingAsync(serverCallID);
                schedule.ServerCallID = serverCallID;
                _dbContext.Update(schedule);
                _dbContext.SaveChanges();
            }
        }

        private async Task CreateAndEmailOTPAsync(LawyerVideoConferenceSchedule schedule)
        {
            var participantOTP = new Dictionary<Guid, string>();
            const string validChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
            foreach (var participant in schedule.LawyerVideoConferenceParticipants)
            {
                var otp = RandomNumberGenerator.GetString(validChars, 5);
                participant.OTPHash = _passwordService.HashPassword(otp);
                participantOTP.Add(participant.LawyerVideoConferenceParticipant_UID.GetValueOrDefault(), otp);
            }
            _queue.QueueInvocableWithPayload<OneTimePasswordInvocable, OTPEmailModel>(new OTPEmailModel
            {
                ParticipantOTP = participantOTP,
                Transaction_UID = schedule.Transaction.Transaction_UID.GetValueOrDefault()
            });
        }

        private async Task CreateMeetingRoomAsync(LawyerVideoConferenceSchedule schedule)
        {
            var roomParticipants = new List<RoomParticipant>();
            foreach (var participant in schedule.LawyerVideoConferenceParticipants.ToSafeList())
            {
                var attendee = await _communicationIdentityClient.CreateUserAsync();
                participant.MeetingRoomTokenID = await GetTokenResponseAsync(attendee);
                participant.MeetingRoomUserID = attendee.Value.Id;
                roomParticipants.Add(new RoomParticipant(attendee) { Role = ParticipantRole.Attendee });
            }

            var presenter = await _communicationIdentityClient.CreateUserAsync();
            schedule.MeetingRoomTokenID = await GetTokenResponseAsync(presenter);
            schedule.MeetingRoomUserID = presenter.Value.Id;
            roomParticipants.Add(new RoomParticipant(presenter) { Role = ParticipantRole.Presenter });

            CommunicationRoom room = await _roomsClient.CreateRoomAsync(DateTime.Now, DateTime.Now.AddHours(2), roomParticipants);
            schedule.MeetingRoomID = room.Id;
            schedule.Status = nameof(VideoConferenceStatus.InProgress);
        }

        private LawyerVideoConferenceSchedule GetOrCreateNewSchedule(Guid transaction_UID)
        {
            var transactionEntity = _dbContext.Transactions
                .Include(t => t.TransactionSignatories)
                .Single(t => t.Transaction_UID == transaction_UID);

            var schedule = _dbContext.LawyerVideoConferenceSchedules
                .Include(sched => sched.Transaction)
                .FirstOrDefault(sched => sched.TransactionID == transactionEntity.TransactionID);
            if (schedule != null)
            {
                return schedule;
            }
            schedule = new LawyerVideoConferenceSchedule
            {
                LawyerVideoConferenceSchedule_UID = Guid.CreateVersion7(DateTime.UtcNow),
                CreatedOn = DateTime.UtcNow,
                LawyerID = transactionEntity.LawyerID.GetValueOrDefault(),
                TransactionID = transactionEntity.TransactionID,
                Status = nameof(VideoConferenceStatus.New)
            };

            var participants = transactionEntity.TransactionSignatories.ConvertAll(signatory => new LawyerVideoConferenceParticipant
            {
                CreatedOn = DateTime.UtcNow,
                Status = nameof(VideoConferenceStatus.New),
                LawyerVideoConferenceParticipant_UID = Guid.CreateVersion7(DateTime.UtcNow),
                ParticipantID = signatory.UserID
            });
            participants.Add(new LawyerVideoConferenceParticipant
            {
                CreatedOn = DateTime.UtcNow,
                Status = nameof(VideoConferenceStatus.New),
                LawyerVideoConferenceParticipant_UID = Guid.CreateVersion7(DateTime.UtcNow),
                ParticipantID = transactionEntity.PrincipalID,
            });

            schedule.MeetingDate = DateTime.UtcNow;
            schedule.LawyerVideoConferenceParticipants = participants.ToList();
            schedule.Transaction = transactionEntity;
            return schedule;
        }

        private async Task<string> GetTokenResponseAsync(Response<CommunicationUserIdentifier> user)
        {
            var tokenResponse = await _communicationIdentityClient.GetTokenAsync(user, new[] { CommunicationTokenScope.VoIP });
            return tokenResponse.Value.Token;
        }

        private async Task<string> StartRecordingAsync(string serverCallID)
        {
            ArgumentException.ThrowIfNullOrWhiteSpace(serverCallID);
            CallLocator callLocator = new ServerCallLocator(serverCallID);
            var uri = _configuration.GetValue<string>("UriRecordingBlobContainer") ?? string.Empty;
            var recordingResult = await _callAutomationClient
                .GetCallRecording().StartAsync(new StartRecordingOptions(callLocator)
                {
                    RecordingContent = RecordingContent.AudioVideo,
                    RecordingStorage = RecordingStorage.CreateAzureBlobContainerRecordingStorage(new Uri(uri)),
                    RecordingFormat = RecordingFormat.Mp4
                });
            return recordingResult.Value.RecordingId;
        }

        private async Task StopRecordingAsync(LawyerVideoConferenceSchedule schedule)
        {
            if (string.IsNullOrEmpty(schedule.ServerCallID))
            {
                Console.WriteLine("ServerCallID is not set for this transaction.");
                return;
            }

            if (string.IsNullOrEmpty(schedule.RecordingID))
            {
                Console.WriteLine("Recording ID is not set for this transaction.");
                return;
            }

            await _callAutomationClient.GetCallRecording().StopAsync(schedule.RecordingID);
        }
    }
}