using AutoMapper;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.BLL.Helpers;
using PARR.Core.Repositories.Interfaces;
using PARR.Core.Services.NextRunServices;
using PARR.Core.Services.RobotTask.Interfaces;
using PARR.Core.Services.Shortcodes;
using PARR.Domain.DTOs.RobotTask;
using PARR.Domain.Entities;
using PARR.Domain.Entities.Base.History;
using PARR.Domain.Enums;
using PARR.Domain.Exceptions;
using PARR.Domain.Settings;

namespace PARR.Core.Services.RobotTask.Implementations
{
    internal class RobotTaskService : IRobotTaskService
    {
        /// <summary>
        /// Количество заданий которые рассматриваем для взятия в работу. 
        /// </summary>
        private readonly int TakeTasks = 10;

        private readonly ILogger<RobotTaskService> logger;
        private readonly IRobotConfigurationRepository robotConfigurationRepository;
        private readonly SettingsFromDb settingsFromDb;
        private readonly IRobotHistoryRepository robotHistoryRepository;
        private readonly IMapper mapper;
        private readonly IShortcodesService shortcodesService;
        private readonly INextRunService nextRunService;

        public RobotTaskService(
            ILogger<RobotTaskService> logger,
            IRobotConfigurationRepository robotConfigurationRepository,
            SettingsFromDb settingsFromDb,
            IRobotHistoryRepository robotHistoryRepository,
            IMapper mapper,
            IShortcodesService shortcodesService,
            INextRunService nextRunService
            )
        {
            this.logger = logger;
            this.robotConfigurationRepository = robotConfigurationRepository;
            this.settingsFromDb = settingsFromDb;
            this.robotHistoryRepository = robotHistoryRepository;
            this.mapper = mapper;
            this.shortcodesService = shortcodesService;
            this.nextRunService = nextRunService;
        }


        public async Task<RobotTaskTemplate> GetTemplateTaskAsync(TaskStatusEnum taskStatusCode, bool acquireTask, string? robotIp, string? robotId)
        {
            var templateTask = await GetTaskAsync(RobotsEnum.TemplateOrder, taskStatusCode, acquireTask, robotIp, robotId);

            var task = mapper.Map<RobotTaskTemplate>(templateTask);

            task = task with { FullDescription = NormalizeLineEndingsToCrlf(await shortcodesService.ApplyShortcodesAsync(task.FullDescription, templateTask.Template!)) };
            task = task with { ShortDescription = await shortcodesService.ApplyShortcodesAsync(task.ShortDescription, templateTask.Template!) };
            task = task with { Solution = NormalizeLineEndingsToCrlf(await shortcodesService.ApplyShortcodesAsync(task.Solution, templateTask.Template!)) };
            task = task with { TnkName = await shortcodesService.ApplyShortcodesAsync(task.TnkName, templateTask.Template!) };
            task = task with { WorkName = await shortcodesService.ApplyShortcodesAsync(task.WorkName, templateTask.Template!) };
            task = task with { WorkGroup = await shortcodesService.ApplyShortcodesAsync(task.WorkGroup, templateTask.Template!) };
            task = task with { ResponseArea = await shortcodesService.ApplyShortcodesAsync(task.ResponseArea, templateTask.Template!) };

            task = task with { ClosingCode = settingsFromDb.ClosingCode };
            task = task with { Initiator = settingsFromDb.Initiator };
            task = task with { Category = settingsFromDb.Category };

            return task;
        }


        public async Task<RobotTaskSchedule> GetScheduleTaskAsync(TaskStatusEnum taskStatusCode, bool acquireTask, string? robotIp, string? robotId, IHistoryInitiator historyInitiator)
        {
            var scheduleTask = await GetTaskAsync(RobotsEnum.ScheduleOrder, taskStatusCode, acquireTask, robotIp, robotId);

            // Проверяем nextRun, lastRun, обновляем их

            var resultUpdateNextRun = await UpdateNextRunAsync(scheduleTask, historyInitiator);
            if (!resultUpdateNextRun)
            {
                logger.LogError("Ошибка при расчете NextRun для templateId: {templateId}", scheduleTask.TemplateId);
                throw new NextRunException($"Ошибка при расчете NextRun для templateId: {scheduleTask.TemplateId}");
            }

            var task = mapper.Map<RobotTaskSchedule>(scheduleTask);

            task = task with { Timezone = settingsFromDb.EsppScheduleTimezone };
            task = task with { WorkGroup = await shortcodesService.ApplyShortcodesAsync(task.WorkGroup, scheduleTask.Template!) };
            task = task with { ResponseArea = await shortcodesService.ApplyShortcodesAsync(task.ResponseArea, scheduleTask.Template!) };

            //nextRun в часовой зоне УЗ Робота ЕСПП
            var nextRunWithRobotTz = scheduleTask.Template!.NextRun.Add(nextRunService.GetEsppAccountOffset());
            //на всякий случай еще раз проверяем, что дата не устарела и отправляем задание
            if (nextRunWithRobotTz < DateTimeOffset.UtcNow)
            {
                logger.LogError("Ошибка при расчете NextRun для templateId: {templateId}, итоговое значение для робота, меньше чем сейчас {nextRunWithRobotTz}<{now}",
                                                            task.TemplateId, nextRunWithRobotTz, DateTimeOffset.UtcNow);
                throw new NextRunException($"Ошибка при расчете NextRun для templateId: {scheduleTask.TemplateId}");
            }

            task = task with { NextStart = EsppScheduleHelpers.GetNextRun(nextRunWithRobotTz) };
            task = task with { GenerationTime = EsppScheduleHelpers.GetGenerationTime(nextRunWithRobotTz) };

            task = task with { RepeatRange = settingsFromDb.ScheduleRepeatRange };
            task = task with { };

            return task;
        }


        /// <summary>
        /// Получить задачу для робота.
        /// Метод может генерировать исключения.
        /// </summary>
        /// <param name="robotCode"></param>
        /// <param name="taskStatusCode"></param>
        /// <param name="acquireTask">Взять в работу</param>
        /// <param name="robotIp"></param>
        /// <returns></returns>
        /// <exception cref="NotFoundException"></exception>
        private async Task<RobotConfiguration> GetTaskAsync(RobotsEnum robotCode, TaskStatusEnum taskStatusCode, bool acquireTask, string? robotIp, string? robotId)
        {
            // 1. Ищем все задания с превышенным кол-вом попыток и просроченным временем, ставим им статус ошибки
            await robotConfigurationRepository.MarkExpiredTasksAsFailedAsync(settingsFromDb.RobotAttemptsNumber, settingsFromDb.RobotWaitTime);


            // 2. Ищем доступные задания
            var availableTasks = await GetAvailableTasksAsync(robotCode, taskStatusCode);

            if (availableTasks.Count == 0)
                throw new NotFoundException("Нет доступных заданий для робота");

            Guid? acquiredTaskId = null;

            if (acquireTask)
            {
                // Берем задание в работу, устанавливаем ей статус "В работе"
                acquiredTaskId = await AcquireTaskAsync(availableTasks, robotIp, robotId);

                if (acquiredTaskId == null)
                    throw new Exception($"Не удалось взять ни одну из доступных задач ({availableTasks.Count}) в работу");
            }
            else
            {
                // Берем первую задачу из списка доступных
                acquiredTaskId = availableTasks.First();
                logger.LogDebug("Задача не требует захвата, взята первая из доступных: {TaskId}", acquiredTaskId);
            }


            //3. Получаем задачу со всеми нужными инклудами в зависимости от типа робота
            var task = await GetTaskWithAllDataAsync(acquiredTaskId.Value, robotCode);

            return task;
        }


        /// <summary>
        /// Получить список возможных заданий для взятия в работу.
        /// Кол-во заданй ограничено переменной TakeTasks
        /// </summary>
        /// <param name="robotCode"></param>
        /// <param name="taskStatusCode"></param>
        /// <returns></returns>
        private async Task<List<Guid>> GetAvailableTasksAsync(RobotsEnum robotCode, TaskStatusEnum taskStatusCode)
        {
            var query = robotConfigurationRepository.Get()
                                .AsNoTracking()
                                .Where(t => t.RobotCode == (int)robotCode && t.TaskStatusCode == (int)taskStatusCode);

            // Если это задание для робота расписаний
            if (robotCode == RobotsEnum.ScheduleOrder)
            {
                // Выбираем только записи с созданными шаблонами (у которых статус 30), а только потом ищем у них расписания
                var createdTemplates = robotConfigurationRepository.Get()
                                            .Where(t => t.RobotCode == (int)RobotsEnum.TemplateOrder && t.TaskStatusCode == (int)TaskStatusEnum.Ok)
                                            .Select(t => t.TemplateId);

                query = query.Where(t => createdTemplates.Contains(t.TemplateId));
            }

            // Сортируем по nextRun, чтобы те, у кого nextRun ближе к текущей, выполнились скорее
            query = query.OrderBy(t => t.Template!.NextRun).ThenBy(t => t.Template!.IsActiveSchedule).ThenBy(t => t.Template!.IsActiveTemplate);

            // Кандидаты заданий
            var tasks = new List<Guid>();

            // Ещем первые 10 заданий в статусе ОЖИДАНИЕ
            tasks = await query.Where(t => t.RobotStatusCode == (int)RobotStatusEnum.Wait).Take(TakeTasks).Select(t => t.Id).ToListAsync();

            logger.LogDebug("Найдено заданий в статусе 'Ожидание' {Count} шт. Робот '{Robot}'", tasks.Count, robotCode.ToString());

            if (tasks.Count == 0)
            {
                // Ищем задания в статусе В РАБОТЕ, которые можно перезапустить
                // Поиск по `RobotStatusCode` = 22.
                // Далее проверяется `LastStatusUpdated`, что время последнего смены статуса не превышает допустимого(берется из настроек, поле `RobotWaitTime`)
                // и что текущая попытка не больше разрешенной(берется из настроек, поле `RobotAttemptsNumber`) - если это так, берется эта запись.

                var endDate = DateTimeOffset.UtcNow.Add(-settingsFromDb.RobotWaitTime);

                tasks = await query.Where(t => t.RobotStatusCode == (int)RobotStatusEnum.InProgress
                                                && t.AttemptsNumber < settingsFromDb.RobotAttemptsNumber
                                                && t.LastRobotStatusUpdated < endDate)
                                    .Take(TakeTasks)
                                    .Select(t => t.Id)
                                    .ToListAsync();

                logger.LogDebug("Найдено заданий в статусе 'В работе' {Count} шт. Робот '{Robot}'", tasks.Count, robotCode.ToString());
            }

            return tasks;
        }


        /// <summary>
        /// Взять задачу в работу
        /// </summary>
        /// <param name="taskId"></param>
        /// <returns></returns>
        private async Task<Guid?> AcquireTaskAsync(List<Guid> tasks, string? robotIp, string? robotId)
        {
            foreach (var taskId in tasks)
            {
                var isChangedStatus = await robotConfigurationRepository.SetInProgressStatusAsync(taskId);
                if (isChangedStatus)
                {
                    logger.LogDebug("Захвачена задача {TaskId}", taskId);

                    var task = await robotConfigurationRepository.Get()
                                            .AsNoTracking()
                                            .FirstAsync(t => t.Id == taskId);

                    // пишем в историю робота
                    var history = new RobotHistory
                    {
                        Id = Guid.NewGuid(),
                        HistoryLevel = (int)RobotHistoryLevelEnum.Start,
                        TaskStatusCode = task.TaskStatusCode,
                        RobotConfigurationId = taskId,
                        RobotIp = robotIp,
                        RobotId = robotId
                    };

                    if (!await robotHistoryRepository.CreateAsync(history) || !await robotHistoryRepository.CommitAsync())
                        throw new DbErrorException("Ошибка при добавлении истории робота, при взятии задания в работу.");

                    return taskId;
                }
                else
                {
                    logger.LogDebug("Не удалось захватить задачу {TaskId}", taskId);
                }
            }

            logger.LogDebug("Не удалось захватить ни одну из доступных задач для робота");

            return null;
        }


        /// <summary>
        /// Получить задачу со всем необходимыми полями
        /// </summary>
        /// <param name="taskId"></param>
        /// <param name="robotCode"></param>
        /// <returns></returns>
        private async Task<RobotConfiguration> GetTaskWithAllDataAsync(Guid taskId, RobotsEnum robotCode)
        {
            IQueryable<RobotConfiguration> query = robotConfigurationRepository.Get()
                                //.AsNoTracking() // нужно обязательно трекать, так как может измениться nextRun и его нужно будет сохранить
                                .AsSingleQuery()
                                // Общие инклуды для шаблонов и расписаний
                                // Units
                                .Include(t => t.Template)
                                    .ThenInclude(t => t!.Unit)
                                        .ThenInclude(t => t!.UnitValues)
                                            .ThenInclude(t => t!.Field)
                                .Include(t => t.Template)
                                    .ThenInclude(t => t!.Unit)
                                        .ThenInclude(t => t!.UnitValues)
                                            .ThenInclude(t => t!.Value)
                                .Include(t => t.Template)
                                    .ThenInclude(t => t!.UnitsInTemplate)
                                // Группа с типом
                                .Include(t => t.Template)
                                    .ThenInclude(t => t!.Job)
                                        .ThenInclude(t => t!.Group)
                                            .ThenInclude(t => t!.GroupType)
                                // ТНК 
                                .Include(t => t.Template)
                                    .ThenInclude(t => t!.Job)
                                        .ThenInclude(t => t!.Tnk)
                                            .ThenInclude(t => t!.Subprocess)
                                                .ThenInclude(t => t!.Process);

            if (robotCode == RobotsEnum.ScheduleOrder)
            {
                // Инклуды только для расписаний
                query = query
                            // Расписание
                            .Include(t => t.Template)
                                .ThenInclude(t => t!.Job)
                                    .ThenInclude(t => t!.Group)
                                        .ThenInclude(t => t!.EsppSchValues)
                                            .ThenInclude(t => t!.EsppSchTypeConfig)
                                                .ThenInclude(t => t!.EsppSchTypeSchedule)
                            // Исключения по типу
                            .Include(t => t.Template)
                                .ThenInclude(t => t!.Job)
                                    .ThenInclude(t => t!.Group)
                                        .ThenInclude(t => t!.ScheduleExcludeType)
                            // Исключения по календарю
                            .Include(t => t.Template)
                                .ThenInclude(t => t!.Job)
                                    .ThenInclude(t => t!.Group)
                                        .ThenInclude(t => t!.ScheduleExcludeTypeCalendar);
            }

            return await query.FirstAsync(t => t.Id == taskId);
        }


        /// <summary>
        /// Приводит переносы строк в тексте к формату CRLF (\r\n)
        /// </summary>
        /// <param name="text">Исходный текст</param>
        /// <returns>Текст с унифицированными переносами строк</returns>
        private string NormalizeLineEndingsToCrlf(string? text)
        {
            if (string.IsNullOrEmpty(text))
                return string.Empty;

            // Заменяем любые варианты переносов (\r\n, \r, \n) на единый \r\n
            return System.Text.RegularExpressions.Regex.Replace(text, @"\r\n|\r|\n", "\r\n");
        }


        /// <summary>
        /// Обоновить NextRun если он устарел
        /// </summary>
        /// <param name="task"></param>
        /// <returns></returns>
        private async Task<bool> UpdateNextRunAsync(RobotConfiguration task, IHistoryInitiator historyInitiator)
        {
            var template = task.Template!;

            //var nextRun = await esppScheduleTransformService.GetNextDateAsync(template.Job!.GroupId, template!.Job!.Group!.ReferenceDate);
            var nextRun = await nextRunService.GetNextRunForTemplateAsync(template.Id, false);

            if (!nextRun.HasValue)
            {
                logger.LogError("При обновлении nextRun для шаблона {templateId}, расчитанный nextRun=null, ошибка в расчетах.", template.Id);
                return false;
            }

            if (nextRun.Value < DateTimeOffset.UtcNow)
            {
                logger.LogError("При обновлении nextRun для шаблона {templateId}, расчитанный nextRun<Now [{nextRun}<{now}], ошибка в расчетах.", template.Id, nextRun.Value, DateTimeOffset.UtcNow);
                return false;
            }

            if (nextRun != template.NextRun)
            {
                logger.LogDebug($"Для шаблона id {template.Id} обновляю nextRun, новое значение {nextRun}, старое значение {template.NextRun}");

                template.LastRun = template.NextRun;
                template.NextRun = nextRun.Value;

                var suffix = "При получении задания роботом, обновил NextRun";
                historyInitiator.InitiatorComment =
                    string.IsNullOrEmpty(historyInitiator.InitiatorComment)
                        ? suffix
                        : $"{historyInitiator.InitiatorComment}. {suffix}";

                if (!await robotConfigurationRepository.CommitAsync(historyInitiator))
                    throw new DbErrorException("Ошибка при сохранении изменения NextRun");
            }

            return true;
        }

    }
}
