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

namespace PARR.Core.Services.NextRunServices.Subservices
{
    /// <summary>
    /// Распределитель шаблонов с поддержкой часовых поясов.
    /// Все входные даты интерпретируются в целевом часовом поясе (параметр offset).
    /// Все результаты возвращаются строго в UTC (TimeSpan.Zero).
    /// </summary>
    internal class TemplateDistributor : ITemplateDistributor
    {
        private readonly ILogger<TemplateDistributor> logger;
        private readonly IWeekendDayRepository weekendDayService;

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

        /// <summary>
        /// Максимальное количество попыток расширения периода при отсутствии рабочих дней
        /// </summary>
        private const int maxPeriodExpansions = 2;

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


        public async Task<TemplateNextRunResultDto> GetNextRunForTemplateAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TimeSpan? offset, TemplateNextRunDto targetTemplate, List<TemplateNextRunDto> allTemplates, bool excludeWeekends, bool isNew)
        {
            // Валидация входных параметров
            if (duration <= 0)
                throw new ArgumentException($"Duration must be positive, got {duration}", nameof(duration));

            if (allTemplates == null)
                throw new ArgumentNullException(nameof(allTemplates));

            logger.LogInformation("Начинаем проверку/расчёт NextRun для шаблона {templateId}, nextRun={nextRun}. isNew={isNew}, offset={offset}, referenceDate={referenceDate}", targetTemplate.Id, targetTemplate.NextRun, isNew, offset, referenceDate);

            // 1. Определяем целевой часовой пояс
            var targetOffset = offset ?? TimeSpan.Zero;
            var referenceDateInTargetTz = referenceDate.ToOffset(targetOffset);

            logger.LogDebug("Целевой часовой пояс: {offset}. ReferenceDate в целевом поясе: {refDate}", targetOffset, referenceDateInTargetTz);


            // 2. Рассчитываем период распределения в целевом поясе
            var _startDate = GetStartPeriodDate(startDate, referenceDateInTargetTz, targetOffset);
            var _endDate = GetEndPeriodDate(_startDate, duration);

            logger.LogInformation("Период распределения: {_startDate} - {_endDate} (в поясе {offset})", _startDate, _endDate, targetOffset);

            // 3. Получаем список рабочих дней с расширением периода при необходимости
            var (expandedEndDate, workDays) = await GetWorkDaysWithExpansionAsync(_startDate, duration, excludeWeekends);

            // Обновляем _endDate на расширенный период для дальнейших расчётов
            _endDate = expandedEndDate;

            if (workDays.Count == 0)
            {
                logger.LogError("Не удалось найти рабочие дни даже после {maxAttempts} попыток расширения периода [{start}, {end}]. Возвращаем ошибку со смещением.", maxPeriodExpansions, _startDate, _endDate);
                return FillTemplateResultWithOffsetError(targetTemplate, _startDate, referenceDateInTargetTz, targetOffset);
            }

            #region 4. Строим план нагрузки и рассчитываем текущую нагрузку

            logger.LogDebug("Строим план распределения для шаблона {templateId}. Всего шаблонов: {totalTemplates}", targetTemplate.Id, allTemplates.Count);

            // Исключаем текущий шаблон из расчёта
            var templatesForPlan = allTemplates.Where(t => t.Id != targetTemplate.Id).ToList();

            logger.LogDebug("Исключили текущий шаблон из расчёта. Осталось {count} шаблонов для плана", templatesForPlan.Count);

            // Определяем тип плана (обычный или зеркальный)
            bool useMirrorPlan = false;

            // Определяем тип плана на основе контекста
            if (!targetTemplate.NextRun.HasValue)
            {
                // Нет текущего NextRun — тип плана зависит от isNew
                useMirrorPlan = !isNew;
                logger.LogDebug("Нет текущего NextRun. Тип плана: {planType} (isNew={isNew})", useMirrorPlan ? "зеркальный" : "обычный", isNew);
            }
            else
            {
                // Есть текущий NextRun — проверяем, в периоде ли он
                var _nextRunInTargetTz = targetTemplate.NextRun.Value.ToOffset(targetOffset);
                var _nextRunDate = DateOnly.FromDateTime(_nextRunInTargetTz.Date);

                bool _isInPeriod = _nextRunDate >= _startDate && _nextRunDate <= _endDate;
                bool _isWorkDay = workDays.Contains(_nextRunDate);

                if (_isInPeriod && !_isWorkDay)
                {
                    // В периоде, но нерабочий день — обычный план
                    useMirrorPlan = false;
                    logger.LogDebug("NextRun в периоде, но на нерабочем дне. Используем обычный план.");
                }
                else if (!_isInPeriod)
                {
                    // Не в периоде — тип плана зависит от isNew
                    useMirrorPlan = !isNew;
                    logger.LogDebug("NextRun вне периода. Тип плана: {planType} (isNew={isNew})", useMirrorPlan ? "зеркальный" : "обычный", isNew);
                }
                else
                {
                    // В периоде и рабочий день — будем проверять актуальность
                    useMirrorPlan = false;
                    logger.LogDebug("NextRun в периоде и на рабочем дне. Используем обычный план для проверки.");
                }
            }

            // Строим план распределения
            List<DateOnly> plan;
            if (useMirrorPlan)
            {
                // Зеркальный план
                plan = GetDistributionPlan(templatesForPlan.Count + 1, workDays, mirror: true);
                logger.LogDebug("Построен зеркальный план для {count} шаблонов", templatesForPlan.Count + 1);
            }
            else
            {
                // Обычный план
                plan = GetDistributionPlan(templatesForPlan.Count + 1, workDays);
                logger.LogDebug("Построен обычный план для {count} шаблонов", templatesForPlan.Count + 1);
            }

            // Считаем ожидаемую нагрузку из плана
            var expectedDailyLoad = plan.GroupBy(d => d).ToDictionary(g => g.Key, g => g.Count());

            logger.LogDebug("Ожидаемая нагрузка по плану: {loadCount} дней", expectedDailyLoad.Count);
            foreach (var kvp in expectedDailyLoad.OrderBy(x => x.Key))
                logger.LogDebug("  {date}: ожидаемо {count} шаблонов", kvp.Key, kvp.Value);

            // Считаем текущую нагрузку (только шаблоны внутри периода)
            var currentDailyLoad = BuildCurrentLoad(templatesForPlan, workDays, _startDate, _endDate, targetOffset);

            logger.LogDebug("Текущая нагрузка (без учёта текущего шаблона): {loadCount} дней", currentDailyLoad.Count);
            foreach (var kvp in currentDailyLoad.OrderBy(x => x.Key))
                logger.LogDebug("  {date}: текущая {current}, ожидаемая {expected}", kvp.Key, kvp.Value, expectedDailyLoad.GetValueOrDefault(kvp.Key, 0));

            #endregion

            // 5. Случай: нет текущего NextRun
            if (!targetTemplate.NextRun.HasValue)
            {
                logger.LogInformation("Шаблон {templateId} не имеет текущего NextRun. Ищем новое место...", targetTemplate.Id);

                return HandleNoNextRun(
                    targetTemplate,
                    workDays,
                    currentDailyLoad,
                    expectedDailyLoad,
                    referenceDateInTargetTz,
                    targetOffset,
                    isNew);
            }

            #region 6. Случай: есть текущий NextRun — проверяем актуальность

            logger.LogDebug("Шаблон {templateId} имеет текущий NextRun: {nextRun}. Проверяем актуальность...", targetTemplate.Id, targetTemplate.NextRun.Value);

            var nextRunInTargetTz = targetTemplate.NextRun.Value.ToOffset(targetOffset);
            var nextRunDate = DateOnly.FromDateTime(nextRunInTargetTz.Date);

            logger.LogDebug("NextRun в целевом поясе {offset}: {nextRunTarget} (дата: {date})", targetOffset, nextRunInTargetTz, nextRunDate);

            // Проверка 1: В периоде?
            var isInPeriod = nextRunDate >= _startDate && nextRunDate <= _endDate;
            logger.LogDebug("Проверка 1 (в периоде): {result} ({date} ∈ [{start}, {end}])", isInPeriod, nextRunDate, _startDate, _endDate);

            // Проверка 2: Рабочий день?
            var isWorkDay = workDays.Contains(nextRunDate);
            logger.LogDebug("Проверка 2 (рабочий день): {result}", isWorkDay ? "да" : "нет");

            // Проверка 3: В будущем?
            var nextRunMoment = new DateTimeOffset(
                                            nextRunDate.Year, nextRunDate.Month, nextRunDate.Day,
                                            nextRunInTargetTz.Hour, nextRunInTargetTz.Minute, nextRunInTargetTz.Second,
                                            targetOffset);

            var isFuture = nextRunMoment >= DateTimeOffset.UtcNow;
            logger.LogDebug("Проверка 3 (в будущем): {result} ({moment} >= {now})", isFuture, nextRunMoment, DateTimeOffset.UtcNow);

            // Проверка 4: Время совпадает с referenceDate?
            var hasMatchingTime = nextRunInTargetTz.Hour == referenceDateInTargetTz.Hour &&
                                  nextRunInTargetTz.Minute == referenceDateInTargetTz.Minute &&
                                  nextRunInTargetTz.Second == referenceDateInTargetTz.Second;
            logger.LogDebug("Проверка 4 (время совпадает): {result} (NextRun: {nextRunHour}:{nextRunMin}:{nextRunSec} vs Reference: {refHour}:{refMin}:{refSec})",
                                                                                            hasMatchingTime,
                                                                                            nextRunInTargetTz.Hour, nextRunInTargetTz.Minute, nextRunInTargetTz.Second,
                                                                                            referenceDateInTargetTz.Hour, referenceDateInTargetTz.Minute, referenceDateInTargetTz.Second);

            // СЛУЧАЙ А: Дата актуальна, но время не совпадает → корректируем ТОЛЬКО время
            if (isInPeriod && isWorkDay && isFuture && !hasMatchingTime)
            {
                logger.LogDebug("Дата NextRun шаблона {templateId} актуальна, но время не совпадает. Корректируем время с {oldTime} на {newTime} (пояс {offset}).",
                    targetTemplate.Id,
                    $"{nextRunInTargetTz.Hour:D2}:{nextRunInTargetTz.Minute:D2}:{nextRunInTargetTz.Second:D2}",
                    $"{referenceDateInTargetTz.Hour:D2}:{referenceDateInTargetTz.Minute:D2}:{referenceDateInTargetTz.Second:D2}",
                    targetOffset);

                // Сохраняем ту же дату, но меняем время на время из referenceDate
                var correctedNextRunInTargetTz = new DateTimeOffset(
                                                            nextRunDate.Year, nextRunDate.Month, nextRunDate.Day,
                                                            referenceDateInTargetTz.Hour,
                                                            referenceDateInTargetTz.Minute,
                                                            referenceDateInTargetTz.Second,
                                                            targetOffset);

                //скорректированный момент ещё в будущем?
                if (correctedNextRunInTargetTz >= DateTimeOffset.UtcNow)
                {
                    // Время актуально — возвращаем скорректированный NextRun
                    var correctedNextRunUtc = correctedNextRunInTargetTz.ToOffset(TimeSpan.Zero);

                    logger.LogInformation("Скорректированный NextRun для шаблона {templateId}: {nextRunTarget} (пояс {offset}) → {nextRunUtc} (UTC). NextRunOld: {nextRunOld}",
                        targetTemplate.Id, correctedNextRunInTargetTz, targetOffset, correctedNextRunUtc, targetTemplate.NextRun);

                    return new TemplateNextRunResultDto(
                        Id: targetTemplate.Id,
                        NextRun: correctedNextRunUtc,
                        NextRunOld: targetTemplate.NextRun);
                }
                else
                {
                    // Скорректированное время уже прошло — считаем дату НЕАКТУАЛЬНОЙ
                    logger.LogDebug("Скорректированное время {correctedTime} уже прошло (сейчас {now}). Считаем дату неактуальной и передаём в основную логику поиска.",
                        correctedNextRunInTargetTz, DateTimeOffset.UtcNow);

                    // Помечаем дату как неактуальную по времени
                    isFuture = false;
                    // Дальнейшая логика сама вызовет HandleOutdatedNextRun с правильными параметрами
                }
            }

            // СЛУЧАЙ Б: Все проверки пройдены (включая время) → возвращаем без изменений
            if (isInPeriod && isWorkDay && isFuture && hasMatchingTime)
            {
                var nextRunUtc = targetTemplate.NextRun.Value.ToOffset(TimeSpan.Zero);
                logger.LogInformation("NextRun шаблона {templateId} полностью актуален. Возвращаем без изменений: {nextRunUtc} (UTC)",
                                                                            targetTemplate.Id, nextRunUtc);

                return new TemplateNextRunResultDto(
                                    Id: targetTemplate.Id,
                                    NextRun: nextRunUtc,
                                    NextRunOld: targetTemplate.NextRun);
            }

            // СЛУЧАЙ В: Дата неактуальна (не в периоде / нерабочий день / уже прошло) → ищем новое место
            logger.LogDebug("NextRun шаблона {templateId} неактуален по дате (в периоде={inPeriod}, рабочий={workDay}, будущее={future}). Ищем новое место...",
                                        targetTemplate.Id, isInPeriod, isWorkDay, isFuture);

            #endregion

            // 7. NextRun неактуален — ищем новое место
            logger.LogDebug("NextRun шаблона {templateId} неактуален (в периоде={inPeriod}, рабочий={workDay}, будущее={future}). Ищем новое место...", targetTemplate.Id, isInPeriod, isWorkDay, isFuture);

            return HandleOutdatedNextRun(
                                targetTemplate,
                                nextRunDate,
                                isInPeriod,
                                isWorkDay,
                                isNew,
                                workDays,
                                currentDailyLoad,
                                expectedDailyLoad,
                                referenceDateInTargetTz,
                                targetOffset,
                                _startDate,
                                _endDate);
        }


        /// <summary>
        /// Распределить шаблоны
        /// </summary>
        /// <param name="startDate"></param>
        /// <param name="duration"></param>
        /// <param name="referenceDate"></param>
        /// <param name="offset"></param>
        /// <param name="allTemplates"></param>
        /// <param name="excludeWeekends"></param>
        /// <returns></returns>
        public async Task<List<TemplateNextRunResultDto>> DistributeTemplatesAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TimeSpan? offset, List<TemplateNextRunDto> allTemplates, bool excludeWeekends)
        {
            // Валидация входных параметров
            if (duration <= 0)
                throw new ArgumentException($"Duration must be positive, got {duration}", nameof(duration));

            if (allTemplates == null)
                throw new ArgumentNullException(nameof(allTemplates));


            // 1. Определяем целевой часовой пояс
            var targetOffset = offset ?? TimeSpan.Zero;

            // 2. Применяем оффсет к referenceDate для расчётов в целевом поясе
            var referenceDateInTargetTz = referenceDate.ToOffset(targetOffset);

            logger.LogDebug("Распределение шаблонов в часовом поясе {offset}. ReferenceDate в целевом поясе: {refDate}", targetOffset, referenceDateInTargetTz);

            // 3. Рассчитываем период распределения С УЧЁТОМ ЧАСОВОГО ПОЯСА
            var _startDate = GetStartPeriodDate(startDate, referenceDateInTargetTz, targetOffset);
            var _endDate = GetEndPeriodDate(_startDate, duration);

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

            // 4. Получаем рабочие дни с расширением периода при необходимости
            var (expandedEndDate, workDays) = await GetWorkDaysWithExpansionAsync(_startDate, duration, excludeWeekends);

            // Обновляем _endDate на расширенный период для дальнейших расчётов
            _endDate = expandedEndDate;

            if (workDays.Count == 0)
            {
                logger.LogError("Не удалось найти рабочие дни даже после {maxAttempts} попыток расширения периода [{start}, {end}]. Возвращаем шаблоны со смещением {years} лет.", maxPeriodExpansions, _startDate, _endDate, offsetYearsIfErrorCalcNextRun);

                var errorResult = allTemplates.Select(t =>
                                                        FillTemplateResultWithOffsetError(t, _startDate, referenceDateInTargetTz, targetOffset)
                                                     ).OrderBy(t => t.NextRun).ToList();

                logger.LogInformation("Распределение завершено с ошибкой. Возвращаем {Count} записей.", errorResult.Count);
                return errorResult;
            }

            // 5. Строим план распределения
            // строим план, дни на которые должны быть назанчены шаблоны
            var plan = GetDistributionPlan(allTemplates.Count, workDays);
            // Подсчитываем, сколько работ должно быть на каждый день
            var expectedDailyLoad = plan.GroupBy(d => d)
                                            .ToDictionary(g => g.Key, g => g.Count());

            #region Группируем шаблоны по дням В ЦЕЛЕВОМ ПОЯСЕ, на которые они уже назначены

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

            foreach (var template in allTemplates)
            {
                if (template.NextRun.HasValue)
                {
                    // КОНВЕРТИРУЕМ В ЦЕЛЕВОЙ ПОЯС ДЛЯ ОПРЕДЕЛЕНИЯ ДАТЫ!
                    var dayInTargetTz = DateOnly.FromDateTime(template.NextRun.Value.ToOffset(targetOffset).Date);
                    //var day = DateOnly.FromDateTime(template.NextRun.Value.Date);

                    if (!templatesByDay.ContainsKey(dayInTargetTz))
                    {
                        templatesByDay[dayInTargetTz] = new List<TemplateNextRunDto>();
                    }
                    templatesByDay[dayInTargetTz].Add(template);

                    logger.LogDebug("Шаблон {Id} с NextRun={nextRun} отнесён к дате {day} в поясе {offset}", template.Id, template.NextRun.Value, dayInTargetTz, targetOffset);
                }
            }

            #endregion

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

            var currentDailyLoad = workDays.ToDictionary(day => day, _ => 0);

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

            #endregion


            #region 8. Определяем, какие шаблоны нужно оставить, а какие — переназначить
            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)
                    {
                        // Конвертируем текущий NextRun в целевой пояс для проверки времени
                        var nextRunInTargetTz = template.NextRun.Value.ToOffset(targetOffset);
                        var nextRunDate = DateOnly.FromDateTime(nextRunInTargetTz.Date);

                        // Проверяем, совпадает ли время с referenceDate
                        var hasMatchingTime = nextRunInTargetTz.Hour == referenceDateInTargetTz.Hour &&
                                              nextRunInTargetTz.Minute == referenceDateInTargetTz.Minute &&
                                              nextRunInTargetTz.Second == referenceDateInTargetTz.Second;

                        DateTimeOffset newNextRun;

                        if (hasMatchingTime)
                        {
                            // Время совпадает — оставляем как есть (только конвертируем в чистый UTC)
                            newNextRun = template.NextRun.Value.ToOffset(TimeSpan.Zero);
                            logger.LogDebug("Шаблон {TemplateId} оставлен без изменений (время совпадает): {NextRun} (UTC)",
                                template.Id, newNextRun);
                        }
                        else
                        {
                            // Время НЕ совпадает — корректируем ТОЛЬКО время, сохраняя ту же дату
                            var correctedNextRunInTargetTz = new DateTimeOffset(
                                nextRunDate.Year, nextRunDate.Month, nextRunDate.Day,
                                referenceDateInTargetTz.Hour,
                                referenceDateInTargetTz.Minute,
                                referenceDateInTargetTz.Second,
                                targetOffset);

                            // Проверяем, что скорректированный момент ещё в будущем
                            if (correctedNextRunInTargetTz >= DateTimeOffset.UtcNow)
                            {
                                newNextRun = correctedNextRunInTargetTz.ToOffset(TimeSpan.Zero);
                                logger.LogDebug("Шаблон {TemplateId} оставлен на дате {date}, время скорректировано с {oldTime} на {newTime} (UTC): {NextRun}",
                                    template.Id, nextRunDate,
                                    $"{nextRunInTargetTz.Hour:D2}:{nextRunInTargetTz.Minute:D2}:{nextRunInTargetTz.Second:D2}",
                                    $"{referenceDateInTargetTz.Hour:D2}:{referenceDateInTargetTz.Minute:D2}:{referenceDateInTargetTz.Second:D2}",
                                    newNextRun);
                            }
                            else
                            {
                                // Скорректированное время уже прошло — переназначаем шаблон
                                templatesToReassign.Add(template);
                                logger.LogDebug("Шаблон {TemplateId} переназначается, т.к. скорректированное время {correctedTime} уже прошло",
                                    template.Id, correctedNextRunInTargetTz);
                                continue;
                            }
                        }

                        templatesToKeep.Add(new TemplateNextRunResultDto(
                            Id: template.Id,
                            NextRun: newNextRun,
                            NextRunOld: template.NextRun
                        ));
                    }
                }
                else
                {
                    // Оставляем только expectedCount штук, остальные — переназначить
                    // (для оставляемых тоже нужно проверить/скорректировать время)
                    for (int i = 0; i < expectedCount; i++)
                    {
                        var template = templates[i];

                        // Конвертируем текущий NextRun в целевой пояс для проверки времени
                        var nextRunInTargetTz = template.NextRun.Value.ToOffset(targetOffset);
                        var nextRunDate = DateOnly.FromDateTime(nextRunInTargetTz.Date);

                        var hasMatchingTime = nextRunInTargetTz.Hour == referenceDateInTargetTz.Hour &&
                                              nextRunInTargetTz.Minute == referenceDateInTargetTz.Minute &&
                                              nextRunInTargetTz.Second == referenceDateInTargetTz.Second;

                        DateTimeOffset newNextRun;

                        if (hasMatchingTime)
                        {
                            newNextRun = template.NextRun.Value.ToOffset(TimeSpan.Zero);
                            logger.LogDebug("Шаблон {TemplateId} (часть перегрузки) оставлен без изменений (время совпадает): {NextRun} (UTC)",
                                template.Id, newNextRun);
                        }
                        else
                        {
                            var correctedNextRunInTargetTz = new DateTimeOffset(
                                nextRunDate.Year, nextRunDate.Month, nextRunDate.Day,
                                referenceDateInTargetTz.Hour,
                                referenceDateInTargetTz.Minute,
                                referenceDateInTargetTz.Second,
                                targetOffset);

                            if (correctedNextRunInTargetTz >= DateTimeOffset.UtcNow)
                            {
                                newNextRun = correctedNextRunInTargetTz.ToOffset(TimeSpan.Zero);
                                logger.LogDebug("Шаблон {TemplateId} (часть перегрузки) оставлен на дате {date}, время скорректировано: {NextRun}",
                                    template.Id, nextRunDate, newNextRun);
                            }
                            else
                            {
                                templatesToReassign.Add(template);
                                logger.LogDebug("Шаблон {TemplateId} (часть перегрузки) переназначается, т.к. скорректированное время уже прошло",
                                    template.Id);
                                continue;
                            }
                        }

                        templatesToKeep.Add(new TemplateNextRunResultDto(
                            Id: template.Id,
                            NextRun: newNextRun,
                            NextRunOld: template.NextRun
                        ));
                    }

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


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

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

            #endregion

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

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

            foreach (var template in templatesToReassign.OrderBy(t => t.NextRun))
            {
                var nextRunInTargetTz = FindNextAvailableDateWithLoad(currentDailyLoad, workDays, referenceDateInTargetTz, expectedDailyLoad, targetOffset);
                if (nextRunInTargetTz.HasValue)
                {
                    var day = DateOnly.FromDateTime(nextRunInTargetTz.Value.Date);
                    currentDailyLoad[day]++;

                    // КОНВЕРТИРУЕМ В UTC ПЕРЕД ВОЗВРАТОМ!
                    updatedTemplates.Add(new TemplateNextRunResultDto
                    (
                        Id: template.Id,
                        NextRun: nextRunInTargetTz.Value.ToOffset(TimeSpan.Zero), // ← В UTC!
                        NextRunOld: template.NextRun
                    ));

                    logger.LogDebug("Шаблону {Id} назначен NextRun: {nextRunTarget} (пояс {offset}) → {nextRunUtc} (UTC). NextRunOld: {NextRunOld}", template.Id, nextRunInTargetTz.Value, targetOffset, nextRunInTargetTz.Value.ToOffset(TimeSpan.Zero), template.NextRun);
                }
                else
                {
                    logger.LogWarning("Не удалось назначить дату для шаблона {TemplateId}", template.Id);

                    // Возвращаем со смещением на несколько лет, так как ошибка
                    // конвертируем в UTC
                    updatedTemplates.Add(FillTemplateResultWithOffsetError(template, _startDate, referenceDateInTargetTz, targetOffset));
                }
            }

            #endregion

            // 11. Сортируем и возвращаем результат в UTC

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

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

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

            return result;
        }


        /// <summary>
        /// Обрабатывает случай, когда у шаблона нет текущего NextRun.
        /// </summary>
        private TemplateNextRunResultDto HandleNoNextRun(TemplateNextRunDto targetTemplate, List<DateOnly> workDays, Dictionary<DateOnly, int> currentDailyLoad, Dictionary<DateOnly, int> expectedDailyLoad, DateTimeOffset referenceDateInTargetTz, TimeSpan targetOffset, bool isNew)
        {
            logger.LogDebug("Обработка случая 'нет текущего NextRun' для шаблона {templateId} (isNew={isNew})", targetTemplate.Id, isNew);

            DateOnly? foundDay = null;

            if (isNew)
            {
                // Ищем от начала периода к концу
                logger.LogDebug("isNew=true: ищем от начала периода к концу");

                foreach (var day in workDays)
                {
                    if (currentDailyLoad[day] < expectedDailyLoad.GetValueOrDefault(day, 0))
                    {
                        foundDay = day;
                        logger.LogDebug("Нашли первый не перегруженный день: {day}", day);
                        break;
                    }
                }

                if (!foundDay.HasValue)
                {
                    // Не нашли свободного места — берём первый день периода
                    foundDay = workDays.First();
                    logger.LogWarning("Все дни перегружены. Берём первый день периода: {day}", foundDay.Value);
                }
            }
            else
            {
                // Ищем от конца периода к началу
                logger.LogDebug("isNew=false: ищем от конца периода к началу");

                foreach (var day in workDays.AsEnumerable().Reverse())
                {
                    if (currentDailyLoad[day] < expectedDailyLoad.GetValueOrDefault(day, 0))
                    {
                        foundDay = day;
                        logger.LogDebug("Нашли первый не перегруженный день: {day}", day);
                        break;
                    }
                }

                if (!foundDay.HasValue)
                {
                    // Не нашли свободного места — берём последний день периода
                    foundDay = workDays.Last();
                    logger.LogWarning("Все дни перегружены. Берём последний день периода: {day}", foundDay.Value);
                }
            }

            // Формируем результат
            var nextRunInTargetTz = new DateTimeOffset(
                                                foundDay.Value.Year, foundDay.Value.Month, foundDay.Value.Day,
                                                referenceDateInTargetTz.Hour,
                                                referenceDateInTargetTz.Minute,
                                                referenceDateInTargetTz.Second,
                                                targetOffset);



            // Корректируем найденный день, чтобы момент времени был в будущем
            DateTimeOffset? adjustedNextRun;
            TemplateNextRunResultDto? errorResult = null;

            adjustedNextRun = AdjustNextRunToFuture(
                foundDay.Value,
                workDays,
                currentDailyLoad,
                expectedDailyLoad,
                referenceDateInTargetTz,
                targetOffset,
                workDays.First(), // startDateForError для случая ошибки
                targetTemplate,
                out errorResult);

            if (errorResult != null)
                return errorResult;

            //var nextRunUtc = nextRunInTargetTz.ToOffset(TimeSpan.Zero);
            var nextRunUtc = adjustedNextRun.Value.ToOffset(TimeSpan.Zero);

            logger.LogInformation("Найдено новое место для шаблона {templateId}: {nextRunTarget} (пояс {offset}) → {nextRunUtc} (UTC)", targetTemplate.Id, nextRunInTargetTz, targetOffset, nextRunUtc);

            return new TemplateNextRunResultDto(
                                    Id: targetTemplate.Id,
                                    NextRun: nextRunUtc,
                                    NextRunOld: targetTemplate.NextRun);
        }


        /// <summary>
        /// Обрабатывает случай, когда текущий NextRun неактуален.
        /// </summary>
        private TemplateNextRunResultDto HandleOutdatedNextRun(TemplateNextRunDto targetTemplate, DateOnly nextRunDate, bool isInPeriod, bool isWorkDay, bool isNew, List<DateOnly> workDays, Dictionary<DateOnly, int> currentDailyLoad, Dictionary<DateOnly, int> expectedDailyLoad, DateTimeOffset referenceDateInTargetTz, TimeSpan targetOffset, DateOnly periodStart, DateOnly periodEnd)
        {
            logger.LogDebug("Обработка случая 'неактуальный NextRun' для шаблона {templateId}", targetTemplate.Id);

            DateOnly? foundDay = null;

            if (isInPeriod && !isWorkDay)
            {
                // СЦЕНАРИЙ А: В периоде, но нерабочий день
                logger.LogDebug("Сценарий А: в периоде ({date}), но нерабочий день. Ищем слева...", nextRunDate);

                // Вычисляем максимальную нагрузку по всему периоду
                var maxLoadInPeriod = expectedDailyLoad.Values.Max();
                logger.LogDebug("Максимальная нагрузка по периоду: {maxLoad}", maxLoadInPeriod);

                // Берём все рабочие дни СТРОГО ЛЕВЕЕ исходной даты
                var leftDays = workDays
                                    .Where(d => d < nextRunDate)
                                    .OrderByDescending(d => d) // Ближайший к исходной дате — первый
                                    .ToList();

                if (leftDays.Any())
                {
                    logger.LogDebug("Слева найдено {count} рабочих дней. Ищем первый не перегруженный...", leftDays.Count);

                    // Ищем первый день где текущая_нагрузка < максимальная_нагрузка_периода
                    foreach (var day in leftDays)
                    {
                        if (currentDailyLoad[day] < maxLoadInPeriod) // ← КЛЮЧЕВОЕ ИЗМЕНЕНИЕ
                        {
                            foundDay = day;
                            logger.LogDebug("Нашли слева день с допустимой нагрузкой (< {maxLoad}): {day}", maxLoadInPeriod, day);
                            break;
                        }
                    }

                    //// Ищем первый не перегруженный
                    //foreach (var day in leftDays)
                    //{
                    //    if (currentDailyLoad[day] < expectedDailyLoad.GetValueOrDefault(day, 0))
                    //    {
                    //        foundDay = day;
                    //        logger.LogDebug("Нашли слева не перегруженный день: {day}", day);
                    //        break;
                    //    }
                    //}

                    // Если не нашли — берём ближайший слева
                    if (!foundDay.HasValue)
                    {
                        foundDay = leftDays.First();
                        logger.LogWarning("Все дни слева перегружены. Берём ближайший: {day}", foundDay.Value);
                    }
                }
                else
                {
                    // Слева нет рабочих дней — берём ближайший справа
                    var rightDays = workDays
                                        .Where(d => d > nextRunDate)
                                        .OrderBy(d => d) // Ближайший к исходной дате — первый
                                        .ToList();

                    if (rightDays.Any())
                    {
                        foundDay = rightDays.First();
                        logger.LogDebug("Слева нет рабочих дней. Берём ближайший справа: {day}", foundDay.Value);
                    }
                    else
                    {
                        // Совсем нет рабочих дней — ошибка
                        logger.LogError("Нет рабочих дней ни слева, ни справа от {date}. Возвращаем ошибку.", nextRunDate);
                        return FillTemplateResultWithOffsetError(targetTemplate, periodStart, referenceDateInTargetTz, targetOffset);
                    }
                }
            }
            else if (!isInPeriod)
            {
                // СЦЕНАРИЙ Б: Не в периоде
                logger.LogDebug("Сценарий Б: не в периоде ({date}). Ищем в зависимости от isNew...", nextRunDate);

                if (isNew)
                {
                    // Ищем от начала периода к концу (обычный план)
                    logger.LogDebug("isNew=true: ищем от начала периода к концу");

                    foreach (var day in workDays)
                    {
                        if (currentDailyLoad[day] < expectedDailyLoad.GetValueOrDefault(day, 0))
                        {
                            foundDay = day;
                            logger.LogDebug("Нашли первый не перегруженный день: {day}", day);
                            break;
                        }
                    }

                    if (!foundDay.HasValue)
                    {
                        foundDay = workDays.First();
                        logger.LogWarning("Все дни перегружены. Берём первый день периода: {day}", foundDay.Value);
                    }
                }
                else
                {
                    // Ищем от конца периода к началу (зеркальный план)
                    logger.LogDebug("isNew=false: ищем от конца периода к началу");

                    foreach (var day in workDays.AsEnumerable().Reverse())
                    {
                        if (currentDailyLoad[day] < expectedDailyLoad.GetValueOrDefault(day, 0))
                        {
                            foundDay = day;
                            logger.LogDebug("Нашли первый не перегруженный день: {day}", day);
                            break;
                        }
                    }

                    if (!foundDay.HasValue)
                    {
                        foundDay = workDays.Last();
                        logger.LogWarning("Все дни перегружены. Берём последний день периода: {day}", foundDay.Value);
                    }
                }
            }
            else
            {
                // Другие случаи (например, время уже прошло)
                logger.LogDebug("NextRun неактуален по другим причинам. Используем логику в зависимости от isNew...");

                if (isNew)
                {
                    foreach (var day in workDays)
                    {
                        if (currentDailyLoad[day] < expectedDailyLoad.GetValueOrDefault(day, 0))
                        {
                            foundDay = day;
                            break;
                        }
                    }
                    foundDay ??= workDays.First();
                }
                else
                {
                    foreach (var day in workDays.AsEnumerable().Reverse())
                    {
                        if (currentDailyLoad[day] < expectedDailyLoad.GetValueOrDefault(day, 0))
                        {
                            foundDay = day;
                            break;
                        }
                    }
                    foundDay ??= workDays.Last();
                }
            }

            // Формируем результат
            var nextRunInTargetTz = new DateTimeOffset(
                                            foundDay.Value.Year, foundDay.Value.Month, foundDay.Value.Day,
                                            referenceDateInTargetTz.Hour,
                                            referenceDateInTargetTz.Minute,
                                            referenceDateInTargetTz.Second,
                                            targetOffset);

            // Корректируем найденный день, чтобы момент времени был в будущем
            DateTimeOffset? adjustedNextRun;
            TemplateNextRunResultDto? errorResult = null;

            adjustedNextRun = AdjustNextRunToFuture(
                foundDay.Value,
                workDays,
                currentDailyLoad,
                expectedDailyLoad,
                referenceDateInTargetTz,
                targetOffset,
                periodStart, // startDateForError для случая ошибки
                targetTemplate,
                out errorResult);

            if (errorResult != null)
                return errorResult;


            //var nextRunUtc = nextRunInTargetTz.ToOffset(TimeSpan.Zero);
            var nextRunUtc = adjustedNextRun.Value.ToOffset(TimeSpan.Zero);

            logger.LogInformation("Найдено новое место для шаблона {templateId}: {nextRunTarget} (пояс {offset}) → {nextRunUtc} (UTC). NextRunOld: {nextRunOld}", targetTemplate.Id, nextRunInTargetTz, targetOffset, nextRunUtc, targetTemplate.NextRun);

            return new TemplateNextRunResultDto(
                                Id: targetTemplate.Id,
                                NextRun: nextRunUtc,
                                NextRunOld: targetTemplate.NextRun);
        }


        /// <summary>
        /// Корректирует найденный день, чтобы момент времени был в будущем.
        /// Если момент времени уже прошёл, ищет следующий рабочий день в будущем с учётом нагрузки.
        /// В случае ошибки (нет будущих рабочих дней) возвращает ошибку через out-параметр.
        /// </summary>
        private DateTimeOffset? AdjustNextRunToFuture(DateOnly foundDay, List<DateOnly> workDays, Dictionary<DateOnly, int> currentDailyLoad, Dictionary<DateOnly, int> expectedDailyLoad, DateTimeOffset referenceDateInTargetTz, TimeSpan targetOffset, DateOnly startDateForError, TemplateNextRunDto targetTemplate, out TemplateNextRunResultDto? errorResult)
        {
            errorResult = null;

            // Создаём момент времени для найденного дня
            var nextRunInTargetTz = new DateTimeOffset(
                foundDay.Year, foundDay.Month, foundDay.Day,
                referenceDateInTargetTz.Hour,
                referenceDateInTargetTz.Minute,
                referenceDateInTargetTz.Second,
                targetOffset);

            // ФИНАЛЬНАЯ ПРОВЕРКА: момент времени должен быть в будущем
            if (nextRunInTargetTz < DateTimeOffset.UtcNow)
            {
                logger.LogWarning("Найденный момент времени {nextRun} уже прошёл (сейчас {now}). Ищем следующий рабочий день в будущем.",
                    nextRunInTargetTz, DateTimeOffset.UtcNow);

                // Берём все рабочие дни СТРОГО ПОСЛЕ найденного дня
                var futureWorkDays = workDays.Where(d => d > foundDay).ToList();

                if (futureWorkDays.Any())
                {
                    // Ищем первый день с допустимой нагрузкой (текущая < ожидаемая)
                    var notOverloadedDays = futureWorkDays
                        .Where(d => currentDailyLoad[d] < expectedDailyLoad.GetValueOrDefault(d, 0))
                        .ToList();

                    var futureDay = notOverloadedDays.Any()
                        ? notOverloadedDays.First()
                        : futureWorkDays.First();

                    logger.LogDebug("Выбран следующий рабочий день в будущем: {day}", futureDay);

                    // Пересоздаём момент времени с новой датой
                    nextRunInTargetTz = new DateTimeOffset(
                        futureDay.Year, futureDay.Month, futureDay.Day,
                        referenceDateInTargetTz.Hour,
                        referenceDateInTargetTz.Minute,
                        referenceDateInTargetTz.Second,
                        targetOffset);
                }
                else
                {
                    // Нет будущих рабочих дней — ошибка
                    logger.LogError("Нет будущих рабочих дней после {date} в периоде. Возвращаем ошибку со смещением.",
                        foundDay);

                    errorResult = FillTemplateResultWithOffsetError(targetTemplate, startDateForError, referenceDateInTargetTz, targetOffset);
                    return null;
                }
            }
            else
            {
                logger.LogDebug("Момент времени {nextRun} актуален (в будущем). Коррекция не требуется.", nextRunInTargetTz);
            }

            return nextRunInTargetTz;
        }


        /// <summary>
        /// Строит текущую нагрузку на дни, учитывая ТОЛЬКО шаблоны внутри периода.
        /// Шаблоны вне периода игнорируются.
        /// </summary>
        private Dictionary<DateOnly, int> BuildCurrentLoad(List<TemplateNextRunDto> templates, List<DateOnly> workDays, DateOnly periodStart, DateOnly periodEnd, TimeSpan targetOffset)
        {
            var load = workDays.ToDictionary(day => day, _ => 0);

            foreach (var template in templates)
            {
                if (!template.NextRun.HasValue)
                    continue;

                // Конвертируем в целевой пояс для определения даты
                var templateDate = DateOnly.FromDateTime(template.NextRun.Value.ToOffset(targetOffset).Date);

                // Учитываем ТОЛЬКО шаблоны внутри периода
                if (templateDate >= periodStart && templateDate <= periodEnd)
                {
                    if (load.ContainsKey(templateDate))
                        load[templateDate]++;
                }
            }

            return load;
        }


        /// <summary>
        /// Заполнить TemplateNextRunResultDto, если нет рабочих дней со смещением несколько лет
        /// </summary>
        /// <param name="template"></param>
        /// <returns></returns>
        private TemplateNextRunResultDto FillTemplateResultWithOffsetError(TemplateNextRunDto template, DateOnly startDate, DateTimeOffset referenceDateInTargetTz, TimeSpan targetOffset)
        {
            // Создаём дату в целевом поясе
            var errorDateInTargetTz = new DateTimeOffset(
                startDate.AddYears(offsetYearsIfErrorCalcNextRun).Year,
                startDate.AddYears(offsetYearsIfErrorCalcNextRun).Month,
                startDate.AddYears(offsetYearsIfErrorCalcNextRun).Day,
                referenceDateInTargetTz.Hour,
                referenceDateInTargetTz.Minute,
                referenceDateInTargetTz.Second,
                targetOffset);

            // КОНВЕРТИРУЕМ В UTC ДЛЯ ВОЗВРАТА
            var errorDateUtc = errorDateInTargetTz.ToOffset(TimeSpan.Zero);

            logger.LogWarning("Шаблон {Id}: нет рабочих дней в периоде, сдвиг на {years} лет → {nextRun} (UTC)", template.Id, offsetYearsIfErrorCalcNextRun, errorDateUtc);

            return new TemplateNextRunResultDto(
                    Id: template.Id,
                    NextRun: errorDateUtc,
                    NextRunOld: template.NextRun
                );
        }


        /// <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 referenceDateInTargetTz, Dictionary<DateOnly, int> expectedDailyLoad, TimeSpan targetOffset)
        {
            foreach (var day in workDays)
            {
                var expected = expectedDailyLoad.GetValueOrDefault(day, 0);
                var current = currentDailyLoad.GetValueOrDefault(day, 0);

                if (current < expected)
                {
                    // Создаём момент времени В ЦЕЛЕВОМ ПОЯСЕ
                    return new DateTimeOffset(
                                        day.Year, day.Month, day.Day,
                                        referenceDateInTargetTz.Hour,
                                        referenceDateInTargetTz.Minute,
                                        referenceDateInTargetTz.Second,
                                        targetOffset);
                }
            }

            // Если все дни загружены — берём день с минимальной нагрузкой
            var minDay = currentDailyLoad.MinBy(x => x.Value).Key;
            return new DateTimeOffset(
                            minDay.Year, minDay.Month, minDay.Day,
                            referenceDateInTargetTz.Hour,
                            referenceDateInTargetTz.Minute,
                            referenceDateInTargetTz.Second,
                            targetOffset);
        }


        /// <summary>
        /// Получить план распределения шаблонов по рабочим дням.
        /// </summary>
        /// <param name="totalWorks">Общее количество шаблонов для распределения</param>
        /// <param name="workDays">Список рабочих дней в периоде</param>
        /// <param name="mirror">Если true — строит зеркальный план (справа налево от конца периода), иначе обычный (слева направо от начала периода)</param>
        /// <returns>Список дат, на которые должны быть назначены шаблоны</returns>
        private List<DateOnly> GetDistributionPlan(int totalWorks, List<DateOnly> workDays, bool mirror = false)
        {
            var totalDays = workDays.Count;
            var interval = CalculateDistributionInterval(totalDays, totalWorks);

            var plan = new List<DateOnly>();

            for (int i = 0; i < totalWorks; i++)
            {
                // Базовый индекс для обычного плана (слева → направо)
                var baseIndex = (int)Math.Floor(i * interval);

                // Применяем зеркалирование, если нужно
                var index = mirror
                    ? totalDays - 1 - baseIndex  // Зеркальный индекс: от конца периода
                    : baseIndex;                 // Обычный индекс: от начала периода

                // Защита от выхода за границы массива
                index = Math.Max(0, Math.Min(index, totalDays - 1));

                plan.Add(workDays[index]);
            }

            // Логгирование
            var planType = mirror ? "Зеркальный" : "Обычный";
            logger.LogDebug("--- {planType} план распределения ({totalWorks} шаблонов, {totalDays} раб. дней) ---", planType, totalWorks, totalDays);

            foreach (var item in plan.GroupBy(t => t).OrderBy(g => g.Key))
                logger.LogDebug("Дата: {date}, кол-во работ: {workCount}", item.Key, item.Count());

            return plan;
        }


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

            logger.LogDebug("Интервал распределения: {interval} (дней на шаблон)", interval);

            return interval;
        }


        /// <summary>
        /// Получить дату начала распределения с учётом целевого часового пояса.
        /// Метод возвращает календарную дату (DateOnly), рассчитанную в контексте указанного оффсета.
        /// </summary>
        /// <param name="startDate">Стартовая дата (интерпретируется в целевом часовом поясе)</param>
        /// <param name="referenceDate">Момент времени в целевом часовом поясе (после применения оффсета)</param>
        /// <param name="offset">Целевой часовой пояс (оффсет)</param>
        /// <returns>Дата начала распределения</returns>
        private DateOnly GetStartPeriodDate(DateOnly startDate, DateTimeOffset referenceDate, TimeSpan offset)
        {
            var dateStart = startDate;

            // 1. Дата начала не может быть раньше даты из referenceDate (в целевом поясе)
            var refDateInTargetTz = DateOnly.FromDateTime(referenceDate.Date);
            if (dateStart < refDateInTargetTz)
            {
                logger.LogDebug("Дата начала {startDate} раньше даты из referenceDate в поясе {offset} ({refDate}). Используем {refDate}.",
                    dateStart, offset, refDateInTargetTz, refDateInTargetTz);
                dateStart = refDateInTargetTz;
            }

            // 2. Дата начала не может быть раньше "сегодня" в целевом часовом поясе
            var todayInTargetTz = DateOnly.FromDateTime(DateTimeOffset.UtcNow.ToOffset(offset).Date);
            if (dateStart < todayInTargetTz)
            {
                logger.LogDebug("Дата начала {startDate} раньше 'сегодня' в поясе {offset} ({today}). Используем {today}.",
                    dateStart, offset, todayInTargetTz, todayInTargetTz);
                dateStart = todayInTargetTz;
            }

            // 3. Если момент времени "начало дня в целевом поясе + время из referenceDate" уже прошёл — добавляем 1 день
            // ВАЖНО: время берём из самого referenceDate (который уже в целевом поясе!)
            var dateTimeStart = new DateTimeOffset(
                dateStart.Year, dateStart.Month, dateStart.Day,
                referenceDate.Hour,        // ← Время из целевого пояса!
                referenceDate.Minute,
                referenceDate.Second,
                offset);                   // ← Оффсет целевого пояса

            // Сравниваем абсолютные моменты времени (работает корректно даже с разными оффсетами)
            if (dateTimeStart < DateTimeOffset.UtcNow)
            {
                logger.LogDebug("Момент времени {dateTimeStart} (в поясе {offset}) уже прошёл (сейчас {now}). Добавляем 1 день.",
                    dateTimeStart, offset, DateTimeOffset.UtcNow);
                dateStart = dateStart.AddDays(1);
            }

            logger.LogDebug("Итоговая дата начала распределения в поясе {offset}: {dateStart}", offset, dateStart);
            return dateStart;
        }


        /// <summary>
        /// Получить дату окончания периода распределения.
        /// Продолжительность включает стартовый день (например, duration=7 означает 7 дней: с 1 по 7 июня).
        /// </summary>
        /// <param name="startDate"></param>
        /// <param name="duration"></param>
        /// <returns></returns>
        private DateOnly GetEndPeriodDate(DateOnly startDate, int duration)
        {
            if (duration <= 0)
            {
                logger.LogWarning("Некорректная продолжительность периода: {duration}. Используем минимальное значение 1.", duration);
                duration = 1;
            }

            // duration — количество дней, включая startDate
            // поэтому вычитаем 1, чтобы получить сдвиг

            var endDate = startDate.AddDays(duration - 1);
            logger.LogDebug("Расчёт даты окончания периода: startDate={startDate}, duration={duration}, endDate={endDate}", startDate, duration, endDate);

            return endDate;
        }


        /// <summary>
        /// Получить список выходных дней
        /// </summary>
        /// <param name="startDate"></param>
        /// <param name="endDate"></param>
        /// <returns></returns>
        public 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();
        }


        public 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;
        }


        /// <summary>
        /// Получает рабочие дни с автоматическим расширением периода при их отсутствии.
        /// Расширяет период до maxPeriodExpansions раз: _startDate + duration * N (N = 1, 2, 3)
        /// </summary>
        private async Task<(DateOnly endDate, List<DateOnly> workDays)> GetWorkDaysWithExpansionAsync(DateOnly startDate, int duration, bool excludeWeekends)
        {
            // Попытка 0: исходный период [_startDate, _startDate + duration - 1]
            var currentEndDate = startDate.AddDays(duration - 1);
            var workDays = await GetWorkDaysAsync(startDate, currentEndDate, excludeWeekends);

            if (workDays.Count > 0)
            {
                logger.LogDebug("Найдено {count} рабочих дней в исходном периоде [{start}, {end}]", workDays.Count, startDate, currentEndDate);
                return (currentEndDate, workDays);
            }

            // Попытки расширения 1..maxPeriodExpansions
            for (int attempt = 1; attempt <= maxPeriodExpansions; attempt++)
            {
                //расширяем период относительно ПРЕДЫДУЩЕГО конца
                currentEndDate = currentEndDate.AddDays(duration);

                logger.LogWarning("Попытка {attempt}/{maxAttempts} расширения периода: нет рабочих дней в [{start}, {end}]. Расширяем до {newEnd}.", attempt, maxPeriodExpansions, startDate, startDate.AddDays(duration - 1), currentEndDate);

                workDays = await GetWorkDaysAsync(startDate, currentEndDate, excludeWeekends);

                if (workDays.Count > 0)
                {
                    logger.LogInformation("Рабочие дни найдены после {attempt} попытки расширения. Период: [{start}, {end}], кол-во рабочих дней: {count}", attempt, startDate, currentEndDate, workDays.Count);
                    return (currentEndDate, workDays);
                }
            }

            // Все попытки исчерпаны — рабочих дней нет
            logger.LogError("После {maxAttempts} попыток расширения рабочие дни не найдены в периоде [{start}, {end}]. Возвращаем ошибку.", maxPeriodExpansions, startDate, currentEndDate);

            return (currentEndDate, new List<DateOnly>()); // Пустой список для обработки ошибки вызывающим кодом
        }
    }
}
