using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.Core.Common.Interfaces.RabbitServices;
using PARR.Core.Repositories.Interfaces;
using PARR.Domain.Common.Rabbit.Messages;
using PARR.Domain.Entities;
using PARR.Domain.Enums;
using PARR.Master.Settings;
using System.Text.Encodings.Web;
using System.Text.Json;

namespace PARR.Master.Services
{
    internal class OrderMasterService : IOrderMasterService
    {
        private readonly ILogger<OrderMasterService> logger;
        private readonly IOrderRepository orderService;
        private readonly IAgentHistoryRepository agentHistoryService;
        private readonly OrderManageSettings orderManageSettings;
        private readonly MqSettings mqSettings;
        private readonly IRabbitService mqService;

        private static readonly JsonSerializerOptions jsonOptions = new JsonSerializerOptions
        {
            Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
        };

        public OrderMasterService(
            ILogger<OrderMasterService> logger,
            IOrderRepository orderService,
            IAgentHistoryRepository agentHistoryService,
            OrderManageSettings orderManageSettings,
            MqSettings mqSettings,
            IRabbitService mqService

            )
        {
            this.logger = logger;
            this.orderService = orderService;
            this.agentHistoryService = agentHistoryService;
            this.orderManageSettings = orderManageSettings;
            this.mqSettings = mqSettings;
            this.mqService = mqService;
        }


        public async Task ManageOrdersAsync()
        {
            // логика работы описана в документации проекта

            logger.LogInformation($"--- --- Начало поиска нарядов --- ---");

            //ищем наряды в статусе 1 и 2
            var ordersToManage = await GetOrdersNewOrWorkStatusesAsync();

            logger.LogInformation($"Ищу наряды со статусом 1 и 2. Найдено нарядов: {ordersToManage.Count()} шт.");

            foreach (var order in ordersToManage)
            {
                //присваиваем им историю работы агента, меняем стаус наряда
                logger.LogInformation($"--- Найден наряд id: {order.Id}, {order.Number} ---");
                await SetHistoryAndUpdateOrderStatus(order);
            }


            //находим наряды где статусы не равны, добавляем их в Rabbit
            var ordersWithStatusNotEqual = await GetOrdresWithStatusCodeNotEqualAsync();
            var objs = ordersWithStatusNotEqual.Select(t => JsonSerializer.Serialize(new OrderManageMq { OrderId = t.Id }, jsonOptions));

            logger.LogInformation($"Ищу наряды где статусы не равны. Найдено нарядов: {ordersWithStatusNotEqual.Count()}. Добавляю их в RabbitMQ.");

            if (objs.Any())
            {
                var sendResult = await mqService.SendAsync(mqSettings, objs.ToArray());
                if (sendResult.IsSuccess)
                {
                    logger.LogInformation($"Наряды переданы в RabbitMQ: {objs.Count()}");
                    logger.LogDebug($"Список нардов переданных в RabbitMQ: {string.Join(", ", objs)}");
                }
                else
                    logger.LogError($"Ошибка при передаче нарядов в Rabbit, не переданные сообщения: {string.Join(", ", sendResult!.NotSendMessages)}");

            }

            logger.LogInformation($"=== Конец работы с нарядами=== ");
        }


        /// <summary>
        /// Находит наряды в статусе 1 и 2, присваивает им историю работы агента, меняет стаус наряда
        /// </summary>
        /// <param name="order"></param>
        /// <returns></returns>
        private async Task SetHistoryAndUpdateOrderStatus(Order order)
        {
            // получаем историю агента, которая не присовена этому наряду
            var newAgentHistory = await GetNewAgentHistoryByOrderAsync(order);

            logger.LogInformation($"Для наряда {order.Id}, {order.Number} найдено новых историй агентов: {newAgentHistory.Count()} шт.");

            // присваиваем наряду
            SetOrderToAgentHistory(ref newAgentHistory, ref order);

            // совмещенный список истории агента по этому наряду
            var allHistory = newAgentHistory;
            order.AgentHistories.ToList().ForEach(item => allHistory.Add(item));
            allHistory = allHistory.OrderBy(t => t.DateCreated).ToList();

            // смотрим историю агента, и тут же меняем статус наряда

            // Ищем статус "Агент начал выполнять задание"
            var isAgentStarted = allHistory.Any(t => t.HistoryLevelId == (int)AgentHistoryLevelEnum.Start);

            // Ищем статус "Агент завершил выполнение задания"
            var isAgentEnded = allHistory.Any(t => t.HistoryLevelId == (int)AgentHistoryLevelEnum.End);

            // Закрыть наряд
            if (isAgentEnded)
                ChangeNextStatus(ref order, OrderStatusEnum.Complete, "");

            // Наряд в работу
            if (isAgentStarted && !isAgentEnded && order.StatusCode == (int)OrderStatusEnum.New)
                ChangeNextStatus(ref order, OrderStatusEnum.InWork, "");

            // Закрываем по таймауту. Если есть начало, и нет завершения по АгентТаймаут, то закрываем наряд.
            if (isAgentStarted == true && isAgentEnded == false)
            {
                var lastStrartDate = allHistory.Last(t => t.HistoryLevelId == (int)AgentHistoryLevelEnum.Start).DateCreated;
                var timeout = (double)order.Template!.Job!.Group!.AgentTimeOutSec!;

                if (lastStrartDate.AddSeconds(timeout) <= DateTimeOffset.UtcNow)
                {
                    var logMsg = $"Закрываю наряд по таймауту ожидания агента. Время старта агента: {lastStrartDate}, допустимый timeOut: {timeout} сек.";
                    ChangeNextStatus(ref order, OrderStatusEnum.Complete, logMsg);
                }
                else
                    logger.LogDebug($"Для наряда {order.Id}, {order.Number} оставляю статус прежним, ожидаю завершение работы агента. Время старта агента: {lastStrartDate}, допустимый timeOut: {timeout} сек.");
            }

            //TODO: тут можно подумать, и закрывать наряды, у которых нет истории агента. сейчас мы такие наряды пропускаем, их будет закрывать руками админ.
            if (!allHistory.Any())
            {
                logger.LogInformation($"Для наряда {order.Id}, {order.Number} нет истории агента. Ничего не делаем.");
            }

            if (!await orderService.CommitAsync())
            {
                logger.LogError($"Ошибка при сохранении наряда, id: {order.Id}");
            }

            logger.LogInformation($"Закончил работу с нарядом {order.Id}, {order.Number}");
        }


        /// <summary>
        /// Присваивает истории агента идентивикатор наряда
        /// </summary>
        private void SetOrderToAgentHistory(ref List<AgentHistory> newAgentHistory, ref Order order)
        {
            foreach (var historyItem in newAgentHistory)
            {
                historyItem.OrderId = order.Id;
                logger.LogDebug($"Для наряда {order.Id}, {order.Number} добавил истрию агента {historyItem.Id}, {historyItem.HistoryLevelId}");

                // если статус агента = завершил работу. то историю после завешения, не записываем, это возможно относится к другому наряду, или нам уже пофиг на неё
                if (historyItem.HistoryLevelId == (int)AgentHistoryLevelEnum.End)
                {
                    logger.LogDebug($"Для наряда {order.Id}, {order.Number} прекращаю добавлять историю агента, так как добавлена запись со статусом Стоп");
                    continue;
                }
            }

            //отбрасываем все записи которые не добавлены этому наряду
            newAgentHistory = newAgentHistory.Where(t => t.OrderId.HasValue).ToList();
        }


        /// <summary>
        /// Получить историю агента не привязанную к наряду, попадающую в дельту существования наряда
        /// </summary>
        /// <param name="order"></param>
        /// <returns></returns>
        private async Task<List<AgentHistory>> GetNewAgentHistoryByOrderAsync(Order order)
        {
            var agentHistory = await agentHistoryService.Get()
                                .Where(t =>
                                            t.TemplateId == order.TemplateId
                                            && t.DateCreated > order.GenerateDate!.Value.Add(-orderManageSettings.FindAgentHistoryStartOffset)
                                            //&& t.DateCreated <= order.GenerateDate!.Value.AddSeconds(order.Template!.ApplicationsInWork!.AgentTimeOutSec!.Value)
                                            && t.DateCreated <= order.GenerateDate!.Value.Add(order.Template!.Job!.Group!.TemplateDurationTimeSpan!.Value)
                                            && t.OrderId == null
                                            )
                                .OrderBy(t => t.DateCreated)
                                .ToListAsync();

            return agentHistory;
        }


        /// <summary>
        /// Изменить статус наряда
        /// </summary>
        /// <param name="order"></param>
        /// <param name="nextStatus"></param>
        private void ChangeNextStatus(ref Order order, OrderStatusEnum nextStatus, string logCompleteMsg)
        {
            if (order.NextStatusCode != (int)nextStatus)
            {
                order.NextStatusCode = (int)nextStatus;
                logger.LogInformation($"Для наряда {order.Id}, {order.Number} меняю статус на {nextStatus}. {logCompleteMsg}");
            }
            else
            {
                logger.LogDebug($"Для наряда {order.Id}, {order.Number} оставляю статус {nextStatus}, он соответствует дейстивительности.");
            }
        }


        /// <summary>
        /// Получить список нарядов, со статусами 1 или 2 и у которых nextStatus<3
        /// </summary>
        private async Task<List<Order>> GetOrdersNewOrWorkStatusesAsync()
        {
            return await GetOrdres()
                            .Where(t =>
                                (t.StatusCode == (int)OrderStatusEnum.New || t.StatusCode == (int)OrderStatusEnum.InWork)
                                && (!t.NextStatusCode.HasValue || t.NextStatusCode < (int)OrderStatusEnum.Stop)
                            )
                            .OrderBy(t => t.DateCreated)
                            .ToListAsync();
        }


        /// <summary>
        /// Получить список нарядов, у которых StatusCode!=NextStatusCode
        /// </summary>
        private async Task<List<Order>> GetOrdresWithStatusCodeNotEqualAsync()
        {
            return await GetOrdres()
                                    .Where(t => t.NextStatusCode.HasValue && t.NextStatusCode.Value != t.StatusCode)
                                    .ToListAsync();
        }


        /// <summary>
        /// Основной запрос на получения списка нарядов со связями, по базовому условию
        /// </summary>
        /// <returns></returns>
        private IQueryable<Order> GetOrdres()
        {
            return orderService.Get()
                        .Include(t => t.Template).ThenInclude(t => t!.Job)
                        .Include(t => t.AgentHistories)
                        .Where(t =>
                            t.TemplateId.HasValue
                            && t.Template!.Job!.Group!.IsAgent == true
                            && t.GenerateDate.HasValue
                            && t.Template!.Job!.Group!.AgentTimeOutSec.HasValue
                            && string.IsNullOrEmpty(t.Template!.Job!.Group!.TemplateDuration) == false
                            && t.ExpirationDate > DateTimeOffset.UtcNow
                        ).OrderBy(t => t.DateCreated);
        }
    }
}
