using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.DAL.NextRunServices.Models;
using PARR.DAL.Services.Interfaces;

namespace PARR.DAL.NextRunServices.Subservices
{
    internal class TemplateDistributor : ITemplateDistributor
    {
        private readonly ILogger<TemplateDistributor> logger;
        private readonly IWeekendDayService weekendDayService;

        /// <summary>
        /// Смещение nextRun на несколько лет вперед, если не смог его рассчитать
        /// </summary>
        private const int offsetYearsIfErrorCalcNextRun = 5;

        public TemplateDistributor(
              ILogger<TemplateDistributor> logger,
                IWeekendDayService weekendDayService
            )
        {
            this.logger = logger;
            this.weekendDayService = weekendDayService;
        }


        public async Task<TemplateNextRunResultDto> GetValidNextRunForTemplateAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TemplateNextRunDto targetTemplate, List<TemplateNextRunDto> allTemplates, bool excludeWeekends, bool isNew)
        {
            //todo: allTemplates - пока не используется. Будет использоваться когда каждый раз будем строить план

            // Пока работает так:
            // Получаем список рабочих дней, если работа попадает в эти дни, то считаем дату валидной
            // Если нет, то перемещаем на последний день периода распределения или первый в зависимости от параметра isNew

            var _startDate = GetStartPeriodDate(startDate, referenceDate);
            var _endDate = GetEndPeriodDate(_startDate, duration);

            logger.LogDebug("Получаю актуальный nextRun для шаблона {templateId}. Текущий nextRun: {nextRun}. Период {duration} дней, с {_startDate} по {_endDate}. Исключить выходные и праздники: {excludeWeekends}. Для нового шаблона: {isNew}", targetTemplate.Id, targetTemplate.NextRun, duration, _startDate, _endDate, excludeWeekends, isNew);

            // Получаем список рабочих дней (исключая выходные и праздники)
            var workDays = await GetWorkDaysAsync(_startDate, _endDate, excludeWeekends);

            if (workDays.Count == 0)
            {
                logger.LogWarning("Нет доступных рабочих дней в диапазоне {_startDate} - {_endDate}. Возвращаем шаблон со смещением: {offsetYearsIfErrorCalcNextRun} лет.", _startDate, _endDate, offsetYearsIfErrorCalcNextRun);

                // смещаем nextRun вперед
                var result = FillTemplateResultWithOffsetError(targetTemplate, _startDate, referenceDate);

                logger.LogInformation("Получение актуального NextRun завершено, templateId: {templateId}. NextRun {nextRun}. OldNextRun у {oldNextRun} шт.", result.Id, result.NextRun, result.NextRunOld);

                return result;
            }

            // если isNew, то берем первый день периода
            if (isNew)
            {
                var result = new TemplateNextRunResultDto
                (
                    Id: targetTemplate.Id,
                    NextRun: FindNextAvailableDateIgnoreLoad(workDays, referenceDate, true), //CreateDateTimeOffset(workDays.First(), referenceDate),
                    NextRunOld: targetTemplate.NextRun
                );

                logger.LogInformation("Получение актуального NextRun завершено, templateId: {templateId}. Это было создание нового, поместили его в начало периода. NextRun {nextRun}. OldNextRun у {oldNextRun} шт.", result.Id, result.NextRun, result.NextRunOld);

                return result;
            }

            // есть ли NextRun и попадает ли он на рабочий день
            // проверяем чтоб было не меньше чем UtcNow
            //TODO: тут еще сделать проверку нагрузки на день
            if (targetTemplate.NextRun.HasValue && workDays.Contains(DateOnly.FromDateTime(targetTemplate.NextRun.Value.Date)) && targetTemplate.NextRun.Value > DateTimeOffset.UtcNow)
            {
                var result = new TemplateNextRunResultDto
                (
                    Id: targetTemplate.Id,
                    NextRun: CreateDateTimeOffset(targetTemplate.NextRun.Value, referenceDate),
                    NextRunOld: targetTemplate.NextRun
                );

                logger.LogInformation("Получение актуального NextRun завершено, templateId: {templateId}. NextRun актуальный, возвращаем его. NextRun {nextRun}. OldNextRun у {oldNextRun} шт.", result.Id, result.NextRun, result.NextRunOld);

                return result;
            }


            //TODO: Временный костыль, перемещаем на конец периода

            var endPeriodResult = new TemplateNextRunResultDto
            (
                Id: targetTemplate.Id,
                NextRun: FindNextAvailableDateIgnoreLoad(workDays, referenceDate, false),
                NextRunOld: targetTemplate.NextRun
            );

            logger.LogInformation("Получение актуального NextRun завершено, templateId: {templateId}. NextRun перемещен на конец периода. NextRun {nextRun}. OldNextRun у {oldNextRun} шт.", endPeriodResult.Id, endPeriodResult.NextRun, endPeriodResult.NextRunOld);

            return endPeriodResult;
        }


        public async Task<List<TemplateNextRunResultDto>> DistributeTemplatesAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, List<TemplateNextRunDto> allTemplates, bool excludeWeekends)
        {
            var _startDate = GetStartPeriodDate(startDate, referenceDate);
            var _endDate = GetEndPeriodDate(_startDate, duration);

            logger.LogInformation("Начинаем распределение {Count} шаблонов на {duration} дней, с {_startDate} по {_endDate}. ReferenceDate: {referenceDate}. Исключить выходные и праздники: {excludeWeekends}.", allTemplates.Count, duration, _startDate, _endDate, referenceDate, excludeWeekends);

            // Получаем список рабочих дней (исключая выходные и праздники)
            var workDays = await GetWorkDaysAsync(_startDate, _endDate, excludeWeekends);

            if (workDays.Count == 0)
            {
                logger.LogWarning("Нет доступных рабочих дней в диапазоне {_startDate} - {_endDate}. Возвращаем список шаблонов со смещением: {offsetYearsIfErrorCalcNextRun} лет.", _startDate, _endDate, offsetYearsIfErrorCalcNextRun);

                // смещаем все nextRun вперед
                var result = allTemplates.Select(t =>
                                                    FillTemplateResultWithOffsetError(t, _startDate, referenceDate)
                                                ).OrderBy(t => t.NextRun).ToList();

                logger.LogInformation("Распределение завершено. Возвращаем {Count} записей. NextRun не изменился у {oldCount} шт. Обновлен NextRun у {changedCount} шт.", result.Count, result.Count(t => t.NextRun == t.NextRunOld), result.Count(t => t.NextRun != t.NextRunOld));

                return result;
            }

            var totalWorks = allTemplates.Count;

            // интервал между шаблонами
            var interval = GetInterval(workDays.Count, totalWorks);

            // строим план, дни на которые должны быть назанчены шаблоны
            var plan = GetPlan(totalWorks, workDays);


            // Подсчитываем, сколько работ должно быть на каждый день
            var expectedDailyLoad = new Dictionary<DateOnly, int>();

            foreach (var day in plan)
                expectedDailyLoad[day] = expectedDailyLoad.GetValueOrDefault(day, 0) + 1;

            #region Группируем шаблоны по дням, на которые они уже назначены

            var templatesByDay = new Dictionary<DateOnly, List<TemplateNextRunDto>>();

            foreach (var template in allTemplates)
            {
                if (template.NextRun.HasValue)
                {
                    var day = DateOnly.FromDateTime(template.NextRun.Value.Date);
                    if (!templatesByDay.ContainsKey(day))
                    {
                        templatesByDay[day] = new List<TemplateNextRunDto>();
                    }
                    templatesByDay[day].Add(template);
                }
            }

            #endregion

            #region Подсчитываем текущую нагрузку на каждый день

            var currentDailyLoad = new Dictionary<DateOnly, int>();

            foreach (var day in workDays)
            {
                currentDailyLoad[day] = 0;
            }

            foreach (var kvp in templatesByDay)
            {
                var day = kvp.Key;
                var count = kvp.Value.Count;
                currentDailyLoad[day] = count;
            }

            #endregion

            #region Определяем, какие шаблоны нужно оставить, а какие — переназначить

            var templatesToKeep = new List<TemplateNextRunResultDto>();
            var templatesToReassign = new List<TemplateNextRunDto>();

            foreach (var kvp in templatesByDay)
            {
                var day = kvp.Key;
                var templates = kvp.Value;

                // Если день — нерабочий, то все шаблоны нужно переназначить
                if (!workDays.Contains(day))
                {
                    foreach (var template in templates)
                    {
                        templatesToReassign.Add(template);
                        logger.LogDebug("Шаблон {TemplateId} назначен на нерабочий день {NextRun} — будет переназначен.", template.Id, template.NextRun);
                    }
                    continue;
                }

                var expectedCount = expectedDailyLoad.GetValueOrDefault(day, 0);
                var currentCount = templates.Count;

                if (currentCount <= expectedCount)
                {
                    // Все шаблоны в этом дне можно оставить
                    foreach (var template in templates)
                    {
                        templatesToKeep.Add(new TemplateNextRunResultDto
                        (
                            Id: template.Id,
                            NextRun: template.NextRun.Value,
                            NextRunOld: template.NextRun
                        ));
                        logger.LogDebug("Шаблон {TemplateId} оставлен на дату {NextRun}, т.к. нагрузка в день не превышена.", template.Id, template.NextRun);
                    }
                }
                else
                {
                    // Оставляем только expectedCount штук, остальные — переназначить
                    for (int i = 0; i < expectedCount; i++)
                    {
                        var template = templates[i];
                        templatesToKeep.Add(new TemplateNextRunResultDto
                        (
                            Id: template.Id,
                            NextRun: template.NextRun.Value,
                            NextRunOld: template.NextRun
                        ));
                        logger.LogDebug("Шаблон {TemplateId} оставлен на дату {NextRun}, т.к. нагрузка в день не превышена.", template.Id, template.NextRun);
                    }

                    for (int i = expectedCount; i < currentCount; i++)
                    {
                        var template = templates[i];
                        templatesToReassign.Add(template);
                        logger.LogDebug("Шаблон {TemplateId} переназначается, т.к. нагрузка в день превышена.", template.Id);
                    }
                }
            }

            #endregion

            #region Добавляем шаблоны, у которых не было даты — тоже нужно распределить

            foreach (var template in allTemplates)
            {
                if (!template.NextRun.HasValue)
                {
                    templatesToReassign.Add(template);
                    logger.LogDebug("Шаблон {TemplateId} не имел даты — будет распределён.", template.Id);
                }
            }

            #endregion

            #region Распределяем шаблоны, которые нужно переназначить, учитывая нагрузку

            var updatedTemplates = new List<TemplateNextRunResultDto>(templatesToKeep);

            foreach (var template in templatesToReassign)
            {
                var nextRun = FindNextAvailableDateWithLoad(currentDailyLoad, workDays, referenceDate, expectedDailyLoad);
                if (nextRun.HasValue)
                {
                    var day = DateOnly.FromDateTime(nextRun.Value.Date);
                    currentDailyLoad[day]++;

                    updatedTemplates.Add(new TemplateNextRunResultDto
                    (
                        Id: template.Id,
                        NextRun: nextRun.Value,
                        NextRunOld: template.NextRun
                    ));

                    logger.LogDebug("Шаблону {TemplateId} назначен NextRun: {NextRun}, NextRunOld: {NextRunOld}", template.Id, nextRun, template.NextRun);
                }
                else
                {
                    logger.LogWarning("Не удалось назначить дату для шаблона {TemplateId}", template.Id);

                    // Возвращаем с ошибкой
                    updatedTemplates.Add(new TemplateNextRunResultDto
                    (
                        Id: template.Id,
                        NextRun: CreateDateTimeOffset(DateTimeOffset.UtcNow.AddYears(offsetYearsIfErrorCalcNextRun), referenceDate),
                        NextRunOld: template.NextRun
                    ));
                }
            }

            #endregion


            updatedTemplates = updatedTemplates.OrderBy(t => t.NextRun).ToList();

            logger.LogDebug("--- Результат распределения ---");
            foreach (var item in updatedTemplates)
                logger.LogDebug("Шаблон {templateId}, NextRun: {NextRun}, NextRunOld: {NextRunOld}", item.Id, item.NextRun, item.NextRunOld);

            logger.LogInformation("Распределение завершено. Возвращаем {Count} записей. NextRun не изменился у {oldCount} шт. Обновлен NextRun у {changedCount} шт.", updatedTemplates.Count, updatedTemplates.Count(t => t.NextRun == t.NextRunOld), updatedTemplates.Count(t => t.NextRun != t.NextRunOld));

            return updatedTemplates;
        }


        /// <summary>
        /// Заполнить TemplateNextRunResultDto, если нет рабочих дней со смещением 
        /// </summary>
        /// <param name="template"></param>
        /// <returns></returns>
        private TemplateNextRunResultDto FillTemplateResultWithOffsetError(TemplateNextRunDto template, DateOnly startDate, DateTimeOffset referenceDate)
        {
            var result = new TemplateNextRunResultDto
            (
                Id: template.Id,
                //NextRun = template.NextRun.HasValue
                //            // если есть nextRun, то прибавляем к нему offsetYearsIfErrorCalcNextRun
                //            ? CreateNextRun(template.NextRun.Value.AddYears(offsetYearsIfErrorCalcNextRun), referenceDate)
                //            // если нет nextRun, берем сегодня и прибавляем offsetYearsIfErrorCalcNextRun
                //            : CreateNextRun(DateTimeOffset.UtcNow.AddYears(offsetYearsIfErrorCalcNextRun), referenceDate),
                NextRun: CreateDateTimeOffset(startDate.AddYears(offsetYearsIfErrorCalcNextRun), referenceDate),
                NextRunOld: template.NextRun
            );

            logger.LogDebug("Нет рабочих дней, сдвинул nextRun у шаблона {Id} на {offsetYearsIfErrorCalcNextRun} лет вперед, nextRun: {NextRun}, nextRunOld: {NextRunOld}", result.Id, offsetYearsIfErrorCalcNextRun, result.NextRun, result.NextRunOld);

            return result;
        }



        /// <summary>
        /// Получает NextRun не учитывая нагрузку на день
        /// </summary>
        /// <param name="workDays"></param>
        /// <param name="referenseDate"></param>
        /// <param name="isSearchFirst">Искать с начала = true, false - искать с конца</param>
        /// <returns></returns>
        private DateTimeOffset FindNextAvailableDateIgnoreLoad(List<DateOnly> workDays, DateTimeOffset referenseDate, bool isSearchFirst)
        {
            // возможно это временная реализация метода, используется для GetValidNextRunForTemplateAsync

            if (!isSearchFirst)
            {
                // берем первый рабочий день с конца и возвращаем его
                var lastDate = CreateDateTimeOffset(workDays.Last(), referenseDate);
                logger.LogDebug("Берем первый рабочий день с конца. Сгенерированный NextRun: {lastDate}", lastDate);
                return lastDate;
            }

            // будем искать сначала 
            // берем первый день, и смотрим не прошло ли уже время
            var firstDate = CreateDateTimeOffset(workDays.First(), referenseDate);
            if (firstDate > DateTimeOffset.UtcNow)
            {
                logger.LogDebug("Берем первый день с начала. Сгенерированный NextRun: {firstDate}", firstDate);
                return firstDate;
            }


            // тогда берем второй день
            if (workDays.Count > 1)
            {
                var secondDate = CreateDateTimeOffset(workDays.ElementAt(1), referenseDate);
                logger.LogDebug("Берем второй день сначала, так как у первого дня уже время прошло. Сгенерированный NextRun: {secondDate}", secondDate);
                return secondDate;
            }

            logger.LogWarning("Вернули nextRun: {firstDate} из первого дня распределения, хотя время уже просрочено. Всего рабочих дней: {workDays}", firstDate, workDays.Count);
            // если вдруг список рабочих дней был не >1, то тогда берем первый и вренем его. Так никогда не должно быть
            return firstDate;
        }




        /// <summary>
        /// Находит следующий день с доступной нагрузкой, учитывая expectedDailyLoad
        /// </summary>
        /// <param name="currentDailyLoad"></param>
        /// <param name="workDays"></param>
        /// <param name="referenceDate"></param>
        /// <param name="expectedDailyLoad"></param>
        /// <returns></returns>
        private DateTimeOffset? FindNextAvailableDateWithLoad(Dictionary<DateOnly, int> currentDailyLoad, List<DateOnly> workDays, DateTimeOffset referenceDate, Dictionary<DateOnly, int> expectedDailyLoad)
        {
            foreach (var day in workDays)
            {
                var expected = expectedDailyLoad.GetValueOrDefault(day, 0);
                var current = currentDailyLoad.GetValueOrDefault(day, 0);

                if (current < expected)
                {
                    return CreateDateTimeOffset(day, referenceDate);
                }
            }

            // Если нет дня с доступной нагрузкой — ищем день с минимальной текущей нагрузкой, но не превышающий план
            foreach (var day in workDays)
            {
                var expected = expectedDailyLoad.GetValueOrDefault(day, 0);
                var current = currentDailyLoad.GetValueOrDefault(day, 0);

                if (current < expected)
                {
                    return CreateDateTimeOffset(day, referenceDate);
                }
            }

            // Если все дни уже загружены по плану — ищем день с минимальной нагрузкой
            var minLoad = currentDailyLoad.Values.Min();
            var minDay = currentDailyLoad.First(x => x.Value == minLoad).Key;

            return CreateDateTimeOffset(minDay, referenceDate);
        }



        /// <summary>
        /// Получить план: дни, на которые должны быть назначены шаблоны
        /// </summary>
        /// <param name="totalWorks"></param>
        /// <param name="workDays"></param>
        /// <returns></returns>
        private List<DateOnly> GetPlan(int totalWorks, List<DateOnly> workDays)
        {
            var totalDays = workDays.Count();

            var interval = GetInterval(totalDays, totalWorks);

            var plan = new List<DateOnly>();

            for (int i = 0; i < totalWorks; i++)
            {
                #region Вынести в метод
                // Вычисляем индекс дня, на который нужно назначить i-ю работу
                //var dayIndex = (int)Math.Round(i * interval);
                var dayIndex = (int)Math.Floor(i * interval);

                // Защита от возможного выхода за границы массива из-за округления
                if (dayIndex >= totalDays)
                    dayIndex = totalDays - 1;
                #endregion

                var date = workDays[dayIndex];
                plan.Add(date);
            }

            logger.LogDebug("--- План распределения ---");
            foreach (var item in plan.GroupBy(t => t))
                logger.LogDebug("Дата: {date}, кол-во работ: {workCount}", item.Key, item.Count());

            return plan;
        }


        /// <summary>
        /// Рассчитать интервал между шаблонами
        /// </summary>
        /// <param name="totalDays">Кол-во рабочих дней</param>
        /// <param name="totalWorks">Кол-во шаблонов</param>
        /// <returns></returns>
        private double GetInterval(int totalDays, int totalWorks)
        {
            var interval = (double)totalDays / totalWorks;

            return interval;
        }


        /// <summary>
        /// Создать DateTimeOffset (из DateTimeOffset + refDate)
        /// </summary>
        /// <param name="date">Откуда взять дату</param>
        /// <param name="refDate">Откуда взять время</param>
        /// <returns></returns>
        private DateTimeOffset CreateDateTimeOffset(DateTimeOffset date, DateTimeOffset refDate)
        {
            return CreateDateTimeOffset(DateOnly.FromDateTime(date.Date), refDate);
        }

        /// <summary>
        /// Создать DateTimeOffset (из DateOnly + refDate)
        /// </summary>
        /// <param name="date"></param>
        /// <param name="refDate"></param>
        /// <returns></returns>
        private DateTimeOffset CreateDateTimeOffset(DateOnly date, DateTimeOffset refDate)
        {
            return new DateTimeOffset(date.Year, date.Month, date.Day, refDate.Hour, refDate.Minute, refDate.Second, TimeSpan.Zero);
        }


        /// <summary>
        /// Получить дату начала распределения относительно StartDate, ReferenceDate и текущей даты
        /// </summary>
        /// <param name="startDate"></param>
        /// <param name="referenceDate"></param>
        /// <returns></returns>
        private DateOnly GetStartPeriodDate(DateOnly startDate, DateTimeOffset referenceDate)
        {
            // дата начала не может быть раньше referenceDate
            // и не может быть раньше чем сегодня

            var dateStart = startDate;

            // если dateStart < чем refDate, то берем refDate
            if (dateStart < DateOnly.FromDateTime(referenceDate.Date))
                dateStart = DateOnly.FromDateTime(referenceDate.Date);

            // если dateStart < чем текущая дата, то берем текущую дату
            if (dateStart < DateOnly.FromDateTime(DateTime.UtcNow.Date))
                dateStart = DateOnly.FromDateTime(DateTime.UtcNow.Date);

            // если текущее время > referenceDate, то прибавляем 1 день
            var dateTimeStart = CreateDateTimeOffset(dateStart, referenceDate);
            if (dateTimeStart < DateTimeOffset.UtcNow)
                dateStart = dateStart.AddDays(1);

            return dateStart;
        }


        /// <summary>
        /// Получить дату конца периода распределения
        /// </summary>
        /// <param name="startDate"></param>
        /// <param name="duration"></param>
        /// <returns></returns>
        private DateOnly GetEndPeriodDate(DateOnly startDate, int duration)
        {
            // duration — количество дней, включая startDate
            // поэтому вычитаем 1, чтобы получить сдвиг

            return startDate.AddDays(duration - 1);
        }


        /// <summary>
        /// Получить список выходных дней
        /// </summary>
        /// <param name="startDate"></param>
        /// <param name="endDate"></param>
        /// <returns></returns>
        private async Task<HashSet<DateOnly>> GetWeekendsAsync(DateOnly startDate, DateOnly endDate)
        {
            var weekends = await weekendDayService.GetWeekends(startDate, endDate).ToListAsync();

            logger.LogDebug("Найдено не рабочих дней: {Count}", weekends.Count);
            foreach (var weekend in weekends)
                logger.LogDebug("Не рабочий день: {weekend}", weekend);

            return weekends.ToHashSet();
        }


        /// <summary>
        /// Возвращает список рабочих дней в диапазоне, исключая выходные и праздники.
        /// </summary>
        /// <param name="start"></param>
        /// <param name="end"></param>
        /// <param name="excludeWeekends">Исключить выходные и праздники</param>
        /// <returns></returns>
        private async Task<List<DateOnly>> GetWorkDaysAsync(DateOnly start, DateOnly end, bool excludeWeekends)
        {
            // Получаем список выходных/праздничных дней
            var weekends = new HashSet<DateOnly>();
            if (excludeWeekends)
                weekends = await GetWeekendsAsync(start, end);

            var result = new List<DateOnly>();
            var date = start;

            while (date <= end)
            {
                var dayOfWeek = date.DayOfWeek;
                if (!weekends.Contains(date))
                {
                    result.Add(date);
                }
                date = date.AddDays(1);
            }

            logger.LogDebug("Найдено рабочих дней: {Count}", result.Count);
            foreach (var workDay in result)
                logger.LogDebug("Рабочий день: {workDay}", workDay);

            return result;
        }

    }
}
