using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces.Schedule;
using PARR.Domain.Enums;

namespace PARR.Core.Services.NextRunServices.Subservices
{
    internal class EsppScheduleTransformService : IEsppScheduleTransformService
    {
        private readonly IEsppSchTypeConfigRepository esppSchTypeConfigService;
        private readonly ILogger<EsppScheduleTransformService> logger;

        private static readonly Dictionary<string, int> monthDict = new Dictionary<string, int>()
        {
            { "Январь", 1 },
            { "Февраль", 2 },
            { "Март", 3 },
            { "Апрель", 4 },
            { "Май", 5 },
            { "Июнь", 6 },
            { "Июль", 7 },
            { "Август", 8 },
            { "Сентябрь", 9 },
            { "Октябрь", 10 },
            { "Ноябрь", 11 },
            { "Декабрь", 12 }
        };

        private static readonly Dictionary<string, int> orderDict = new Dictionary<string, int>()
        {
            { "Первый", 1 },
            { "Второй", 2 },
            { "Третий", 3 },
            { "Четвертый", 4 },
            { "Последний", 5 }
        };

        private static readonly Dictionary<string, int> dwDict = new Dictionary<string, int>()
        {
            {"Понедельник", 1 },
            {"Вторник", 2 },
            {"Среда", 3 },
            {"Четверг", 4 },
            {"Пятница", 5 },
            {"Суббота", 6 },
            {"Воскресенье", 0 }
         };


        public EsppScheduleTransformService(
            ILogger<EsppScheduleTransformService> logger,
            IEsppSchTypeConfigRepository esppSchTypeConfigService
            )
        {
            this.esppSchTypeConfigService = esppSchTypeConfigService;
            this.logger = logger;
        }

        public async Task<DateTimeOffset> GetNextDateAsync(Guid jobGroupId, DateTimeOffset referenceDate, TimeSpan? offset = null)
        {
            var esppSchedule = await GetEsppScheduleAsync(jobGroupId);

            return GetNextDate(esppSchedule, referenceDate, offset);
        }

        private DateTimeOffset GetNextDate(EsppScheduleDto esppSchedule, DateTimeOffset referenceDate, TimeSpan? offset)
        {
            //offset - желательно передавать всегда, хотябы МСК, чтобы рассчитывались правильно даты, так как в ЕСПП все в МСК

            logger.LogDebug("Начинаю рассчет nextRun по расписанию ЕСПП. Входные данные: date: {referenceDate}, offset: {offset}", referenceDate, offset);

            var nextRun = referenceDate;

            var finalOffset = offset ?? TimeSpan.Zero;

            var offsetReferenceDate = referenceDate.ToOffset(finalOffset);

            logger.LogDebug("Дата после добавления offset - {offsetReferenceDate}", offsetReferenceDate);


            switch (esppSchedule.TypeSchedule.Id)
            {
                case (int)EsppSchTypeScheduleEnum.Regularly:
                    nextRun = GetNextDateRegularly(esppSchedule.Values, offsetReferenceDate);
                    break;
                case (int)EsppSchTypeScheduleEnum.Weekly:
                    nextRun = GetNextDateWeekly(esppSchedule.Values, offsetReferenceDate);
                    break;
                case (int)EsppSchTypeScheduleEnum.Monthly:
                    nextRun = GetNextDateMonthly(esppSchedule.Values, offsetReferenceDate);
                    break;
                case (int)EsppSchTypeScheduleEnum.Monthly2:
                    nextRun = GetNextDateMonthly2(esppSchedule.Values, offsetReferenceDate);
                    break;
                case (int)EsppSchTypeScheduleEnum.Annually:
                    nextRun = GetNextDateAnnually(esppSchedule.Values, offsetReferenceDate);
                    break;
                case (int)EsppSchTypeScheduleEnum.Annually2:
                    nextRun = GetNextDateAnnually2(esppSchedule.Values, offsetReferenceDate);
                    break;
                default:
                    logger.LogError("Неподдерживаемый тип расписания ЕСПП: {TypeId}", esppSchedule.TypeSchedule.Id);
                    throw new NotSupportedException($"Тип расписания {esppSchedule.TypeSchedule.Id} не поддерживается.");
            }

            logger.LogDebug("Рассчитанная дата, с учетом offset: {nextRun}", nextRun);

            // Возвращаем обратно в ЮТС
            nextRun = nextRun.ToOffset(TimeSpan.Zero);

            logger.LogDebug("Рассчитанная дата с нулевым offset: {nextRun}", nextRun);

            return nextRun;
        }


        /// <summary>
        /// Получить расписание из БД
        /// </summary>
        /// <param name="jobGroupId"></param>
        /// <returns></returns>
        /// <exception cref="Exception"></exception>
        private async Task<EsppScheduleDto> GetEsppScheduleAsync(Guid jobGroupId)
        {
            var esppSchedule = await esppSchTypeConfigService.GetEsppScheduleDtoAsync(jobGroupId);
            if (esppSchedule == null)
            {
                logger.LogError($"Не найдено расписание в БД, jobId: {jobGroupId}");
                throw new Exception($"Не найдено расписание в БД, jobId: {jobGroupId}");
            }

            return esppSchedule;
        }


        /// <summary>
        /// Регулярно. Каждые 1 час, 2 часа, Каждые пол года...
        /// </summary>
        /// <param name="values"></param>
        /// <param name="referenceDate"></param>
        /// <returns></returns>
        /// <exception cref="ArgumentException"></exception>
        private DateTimeOffset GetNextDateRegularly(List<EsppScheduleValDto> values, DateTimeOffset referenceDate)
        {
            var regDict = new Dictionary<string, int>
            {
                {"Каждый час",1},
                {"Каждые 2 часа",2},
                {"Каждые 3 часа", 3},
                {"Каждые 4 часа", 4},
                {"Каждые 6 часов", 6},
                {"Каждые 12 часов", 12},
                {"Ежедневно", 24},
                {"Каждые 72 часа", 72},
                {"Каждые 96 часов", 96},
                {"Каждые 2 недели", 336},
                {"Каждые 60 дней", 1440},
                {"Каждые 80 дней", 1920},
                {"Каждые 90 дней", 2160},
                {"Каждые полгода", 4380},
                {"Каждые 1,5 года", 13140},
                {"Каждые 3 года", 26280},
            };

            var key = values[0].Value.Value;
            if (!regDict.TryGetValue(key, out int regHours))
            {
                logger.LogError("Неизвестное значение: {Value}", key);
                throw new ArgumentException($"Неизвестное значение: {key}");
            }

            #region Старая логика

            //var calcDay = referenceDate;

            ////TODO: вот это повторяется от метода к методу
            ////Если итоговая дата указывает на прошлое, то повторяем расчёт и уходим в рекурсию
            //if (calcDay < DateTimeOffset.UtcNow /* || calcDay <= referenceDate*/)
            //    calcDay = GetNextDateRegularly(values, calcDay.AddHours(regHours));

            //return calcDay;

            #endregion

            var nowInTargetZone = DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset);

            var calcDay = referenceDate;

            // Если referenceDate далеко в прошлом, не шагаем по минутам в цикле.
            // Сразу вычисляем, сколько целых интервалов нужно прибавить, чтобы догнать текущее время.
            if (calcDay < nowInTargetZone)
            {
                double totalHoursPast = (nowInTargetZone - calcDay).TotalHours;

                // Считаем, сколько полных интервалов помещается в этот отрезок времени
                long intervalsToSkip = (long)Math.Ceiling(totalHoursPast / regHours);

                calcDay = calcDay.AddHours(intervalsToSkip * regHours);
            }

            // Проверяем условия и делаем микро-шаги, если необходимо
            while (true)
            {
                if (calcDay >= referenceDate && calcDay >= nowInTargetZone)
                {
                    return calcDay;
                }
                calcDay = calcDay.AddHours(regHours);
            }
        }


        /// <summary>
        /// Еженедельно, каждый понедельник, вторник, среду...
        /// </summary>
        /// <param name="values"></param>
        /// <param name="referenceDate"></param>
        /// <returns></returns>
        /// <exception cref="ArgumentException"></exception>
        private DateTimeOffset GetNextDateWeekly(List<EsppScheduleValDto> values, DateTimeOffset referenceDate)
        {
            if (!dwDict.TryGetValue(values[0].Value.Value, out int dwNum))
            {
                logger.LogError("Неизвестное значение дня недели: {Value}", values[0].Value.Value);
                throw new ArgumentException($"Неизвестное значение: {values[0].Value.Value}");
            }

            #region Старая логика
            //var calcDay = referenceDate;
            //if (calcDay < DateTimeOffset.UtcNow)
            //{
            //    var nowInTargetTz = DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset);
            //    calcDay = new DateTimeOffset(
            //                                    nowInTargetTz.Year,
            //                                   nowInTargetTz.Month,
            //                                   nowInTargetTz.Day,
            //                                   referenceDate.Hour,
            //                                   referenceDate.Minute,
            //                                   referenceDate.Second,
            //                                   referenceDate.Offset
            //                               );
            //}


            //if (((int)calcDay.DayOfWeek) != dwNum)
            //    calcDay = OffsetToDayOfWeek(dwNum, calcDay);

            ////Если итоговая дата указывает на прошлое, то повторяем расчёт и ухоим в рекурсию
            //if (calcDay < DateTimeOffset.UtcNow || calcDay <= referenceDate)
            //    calcDay = GetNextDateWeekly(values, calcDay.AddDays(7));//а вот тут что-то новенькое

            //return calcDay;
            #endregion

            var nowInTargetZone = DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset);

            var calcDay = referenceDate;

            // Сдвигаем дату вперед до нужного дня недели
            if ((int)calcDay.DayOfWeek != dwNum)
            {
                calcDay = OffsetToDayOfWeek(dwNum, calcDay);
            }

            // Если получившаяся дата все еще глубоко в прошлом, 
            // не шагаем неделями в цикле, а мгновенно прыгаем ближе к текущему времени математикой.
            if (calcDay < nowInTargetZone)
            {
                double totalDaysPast = (nowInTargetZone - calcDay).TotalDays;

                // Вычисляем, сколько полных недель (по 7 дней) нужно прибавить
                long weeksToSkip = (long)Math.Ceiling(totalDaysPast / 7);

                calcDay = calcDay.AddDays(weeksToSkip * 7);
            }

            while (true)
            {
                // Условие выхода: дата в будущем относительно точки старта И текущего времени
                if (calcDay >= referenceDate && calcDay >= nowInTargetZone)
                {
                    return calcDay;
                }

                // Если условия не выполнены, делаем шаг ровно в одну неделю (7 дней)
                calcDay = calcDay.AddDays(7);
            }
        }


        /// <summary>
        /// Ежемесячно, 1, 2, 3... числа
        /// </summary>
        /// <param name="values"></param>
        /// <param name="referenceDate"></param>
        /// <returns></returns>
        /// <exception cref="ArgumentException"></exception>
        private DateTimeOffset GetNextDateMonthly(List<EsppScheduleValDto> values, DateTimeOffset referenceDate)
        {
            if (!int.TryParse(values[0].Value.Value, out int targetDay))
            {
                logger.LogError("Неизвестное значение дня месяца: {Value}", values[0].Value.Value);
                throw new ArgumentException($"Неизвестное значение: {values[0].Value.Value}");
            }

            #region старая логика, не учтено попадание например 31 числа на февраль. Нет сравнения что расчитанная дата новее или равна refDate
            //var calcDay = new DateTimeOffset(referenceDate.Year, referenceDate.Month, targetDay, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset);
            //if (referenceDate < DateTimeOffset.UtcNow)
            //{
            //    var nowInTargetTz = DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset);
            //    calcDay = new DateTimeOffset(
            //                                    nowInTargetTz.Year,
            //                                    nowInTargetTz.Month,
            //                                    targetDay,
            //                                    referenceDate.Hour,
            //                                    referenceDate.Minute,
            //                                    referenceDate.Second,
            //                                    referenceDate.Offset
            //                                );
            //}

            ////Если итоговая дата указывает на прошлое, то повторяем расчёт и ухоим в рекурсию
            //if (calcDay < DateTimeOffset.UtcNow /*|| calcDay <= referenceDate*/)
            //    calcDay = GetNextDateMonthly(values, calcDay.AddMonths(1));

            //return calcDay;
            #endregion

            // Начинаем проверку с текущего месяца и года переданной referenceDate
            var baseDate = referenceDate;
            int monthsToAdd = 0;

            var nowInTargetZone = DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset);

            while (true)
            {
                // Каждую итерацию мы отталкиваемся от исходной точки и прибавляем +1, +2, +3 месяца
                var nextMonthDate = baseDate.AddMonths(monthsToAdd);
                int year = nextMonthDate.Year;
                int month = nextMonthDate.Month;

                // Если в этом конкретном месяце НЕТ нужного нам дня (например, 31 числа в апреле или 30 в феврале)
                if (targetDay > DateTime.DaysInMonth(year, month))
                {
                    // Пропускаем этот месяц и идем на следующий круг цикла
                    monthsToAdd++;
                    continue;
                }

                // Если день существует, безопасно собираем дату
                var calcDay = new DateTimeOffset(
                    year, month, targetDay,
                    referenceDate.Hour, referenceDate.Minute, referenceDate.Second,
                    referenceDate.Offset
                );

                // Условие выхода: дата строго в будущем относительно referenceDate и текущего UTC-времени
                if (calcDay >= referenceDate && calcDay >= nowInTargetZone)
                {
                    return calcDay;
                }

                // Если дата правильная, но она в прошлом — переходим к следующему месяцу
                monthsToAdd++;
            }
        }


        /// <summary>
        /// Ежемесячно-2, Первый понедельник, Третий вторник
        /// </summary>
        /// <param name="values"></param>
        /// <param name="referenceDate"></param>
        /// <returns></returns>
        /// <exception cref="ArgumentException"></exception>
        /// <exception cref="InvalidOperationException"></exception>
        private DateTimeOffset GetNextDateMonthly2(List<EsppScheduleValDto> values, DateTimeOffset referenceDate)
        {
            if (!orderDict.TryGetValue(values[0].Value.Value, out int orderNum))
            {
                logger.LogError("Неизвестное значение порядка дня: {Value}", values[0].Value.Value);
                throw new ArgumentException($"Неизвестное значение: {values[0].Value.Value}");
            }

            if (!dwDict.TryGetValue(values[1].Value.Value, out int dwNum))
            {
                logger.LogError("Неизвестное значение дня недели: {Value}", values[1].Value.Value);
                throw new ArgumentException($"Неизвестное значение: {values[1].Value.Value}");
            }

            #region Старая логика
            //var startDay = referenceDate < DateTimeOffset.UtcNow
            //                                    ? new DateTimeOffset(
            //                                            DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset).Year,
            //                                            DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset).Month,
            //                                            1,
            //                                            referenceDate.Hour,
            //                                            referenceDate.Minute,
            //                                            referenceDate.Second,
            //                                            referenceDate.Offset
            //                                        )
            //                                    : new DateTimeOffset(
            //                                            referenceDate.Year,
            //                                            referenceDate.Month,
            //                                            1,
            //                                            referenceDate.Hour,
            //                                            referenceDate.Minute,
            //                                            referenceDate.Second,
            //                                            referenceDate.Offset
            //                                        );

            //var daysList = FindAllDaysInMonth(dwNum, startDay);

            //DateTimeOffset calcDay;
            //if (orderNum == 5) // Последний
            //{
            //    calcDay = daysList.Last();
            //}
            //else
            //{
            //    if (orderNum > daysList.Count)
            //    {
            //        logger.LogError("Запрошенный порядковый день недели ({OrderNum}) не существует в месяце {Month}/{Year}. Найдено только {Count} дней.", orderNum, startDay.Month, startDay.Year, daysList.Count);
            //        throw new InvalidOperationException($"Запрошенный порядковый день недели ({orderNum}) не существует в месяце {startDay.Month}/{startDay.Year}. Найдено только {daysList.Count} дней.");
            //    }
            //    else
            //        calcDay = daysList[orderNum - 1];
            //}

            //// Если дата <= текущей, переходим на следующий месяц
            //if (calcDay <= DateTimeOffset.UtcNow /*|| calcDay <= referenceDate*/)
            //    return GetNextDateMonthly2(values, startDay.AddMonths(1));

            //return calcDay;
            #endregion

            var nowInTargetZone = DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset);

            // Начинаем расчет с текущего месяца referenceDate
            int monthsToAdd = 0;

            while (true)
            {
                var currentMonthDate = referenceDate.AddMonths(monthsToAdd);

                // Безопасно формируем первое число для проверяемого месяца
                var startDayOfMonth = new DateTimeOffset(
                    currentMonthDate.Year,
                    currentMonthDate.Month,
                    1,
                    referenceDate.Hour,
                    referenceDate.Minute,
                    referenceDate.Second,
                    referenceDate.Offset
                );

                // Ищем все нужные дни недели в этом месяце
                var daysList = FindAllDaysInMonth(dwNum, startDayOfMonth);

                DateTimeOffset calcDay;

                if (orderNum == 5) // Последний день недели в месяце (например, последняя пятница)
                {
                    calcDay = daysList.Last();
                }
                else
                {
                    // Умная защита: если в текущем месяце нет 5-го вторника, 
                    // мы не выкидываем ошибку приложения, а мягко идем искать в следующем месяце
                    if (orderNum > daysList.Count)
                    {
                        monthsToAdd++;
                        continue;
                    }

                    calcDay = daysList[orderNum - 1];
                }

                // 3. Условие выхода (точно такое же, как во всех предыдущих методах)
                if (calcDay >= referenceDate && calcDay >= nowInTargetZone)
                {
                    return calcDay;
                }

                // Если дата правильная, но она в прошлом — переходим к следующему календарному месяцу
                monthsToAdd++;
            }
        }


        /// <summary>
        /// Ежегодно, Январь 17, Февраль 20
        /// </summary>
        /// <param name="values"></param>
        /// <param name="referenceDate"></param>
        /// <returns></returns>
        /// <exception cref="ArgumentException"></exception>
        private DateTimeOffset GetNextDateAnnually(List<EsppScheduleValDto> values, DateTimeOffset referenceDate)
        {

            if (!monthDict.TryGetValue(values[0].Value.Value, out int monthNum))
            {
                logger.LogError("Неизвестное значение номера месяца: {Value}", values[0].Value.Value);
                throw new ArgumentException($"Неизвестное значение: {values[0].Value.Value}");
            }

            if (!int.TryParse(values[1].Value.Value, out int dayNum))
            {
                logger.LogError("Неизвестное значение дня месяца: {Value}", values[0].Value.Value);
                throw new ArgumentException($"Неизвестное значение: {values[0].Value.Value}");
            }

            #region Старая логика
            ////сравниваем года в одном часовом поясе
            //var nowInTargetTz = DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset);
            //var year = (referenceDate.Year < nowInTargetTz.Year)
            //                                    ? nowInTargetTz.Year
            //                                    : referenceDate.Year;

            //var calcDay = new DateTimeOffset(
            //    year, monthNum, dayNum,
            //    referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset);

            ////Если итоговая дата указывает на прошлое, то повторяем расчёт и ухоим в рекурсию
            //if (calcDay < DateTimeOffset.UtcNow /*|| calcDay <= referenceDate*/)
            //    calcDay = GetNextDateAnnually(values, calcDay.AddYears(1));

            //return calcDay;
            #endregion

            var nowInTargetZone = DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset);

            // Начинаем расчет с года переданной referenceDate
            int yearsToAdd = 0;

            while (true)
            {
                // Вычисляем текущий проверяемый год относительно исходной referenceDate
                var currentYearDate = referenceDate.AddYears(yearsToAdd);
                int year = currentYearDate.Year;

                // ЗАЩИТА: Если в этом году нет такого дня (например, 29 февраля в невисокосном году)
                if (dayNum > DateTime.DaysInMonth(year, monthNum))
                {
                    // Просто пропускаем этот год и переходим к следующему
                    yearsToAdd++;
                    continue;
                }

                // Если день существует, безопасно собираем дату
                var calcDay = new DateTimeOffset(
                    year,
                    monthNum,
                    dayNum,
                    referenceDate.Hour,
                    referenceDate.Minute,
                    referenceDate.Second,
                    referenceDate.Offset
                );

                // Проверяем условия выхода (абсолютно одинаково для всех методов)
                if (calcDay >= referenceDate && calcDay >= nowInTargetZone)
                {
                    return calcDay;
                }

                // Если дата в прошлом — переходим на следующий год
                yearsToAdd++;
            }
        }


        /// <summary>
        /// Ежегодно-2, Первый понедельник января
        /// </summary>
        /// <param name="values"></param>
        /// <param name="referenceDate"></param>
        /// <returns></returns>
        /// <exception cref="ArgumentException"></exception>
        private DateTimeOffset GetNextDateAnnually2(List<EsppScheduleValDto> values, DateTimeOffset referenceDate)
        {
            var monthADict = new Dictionary<string, int>
            {
                { "Января", 1 },
                { "Февраля", 2 },
                { "Марта", 3 },
                { "Апреля", 4 },
                { "Мая", 5 },
                { "Июня", 6 },
                { "Июля", 7 },
                { "Августа", 8 },
                { "Сентября", 9 },
                { "Октября", 10 },
                { "Ноября", 11 },
                { "Декабря", 12 }
            };

            if (!monthADict.TryGetValue(values[2].Value.Value, out int monthNum))
            {
                logger.LogError("Неизвестное значение номера месяца: {Value}", values[2].Value.Value);
                throw new ArgumentException($"Неизвестное значение: {values[2].Value.Value}");
            }
            if (!orderDict.TryGetValue(values[0].Value.Value, out int orderNum))
            {
                logger.LogError("Неизвестное значение порядка дня: {Value}", values[0].Value.Value);
                throw new ArgumentException($"Неизвестное значение: {values[0].Value.Value}");
            }
            if (!dwDict.TryGetValue(values[1].Value.Value, out int dwNum))
            {
                logger.LogError("Неизвестное значение дня недели: {Value}", values[1].Value.Value);
                throw new ArgumentException($"Неизвестное значение: {values[1].Value.Value}");
            }


            #region Старая логика

            //// сравниваем года в одном часовом поясе
            //var nowInTargetTz = DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset);
            //var year = (referenceDate.Year < nowInTargetTz.Year)
            //                                     ? nowInTargetTz.Year
            //                                     : referenceDate.Year;

            ////if (year == DateTimeOffset.UtcNow.Year && referenceDate.Month < DateTimeOffset.UtcNow.Month)
            //if (year == nowInTargetTz.Year && referenceDate.Month < nowInTargetTz.Month)
            //    year += 1;

            //var calcDay = new DateTimeOffset(
            //    year, monthNum, 1,
            //    referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset);

            //List<DateTimeOffset> daysList = FindAllDaysInMonth(dwNum, calcDay);

            //if (orderNum == 5)//5-последний
            //    calcDay = daysList.Last();
            //else
            //    calcDay = daysList[orderNum - 1];

            ////Если итоговая дата указывает на прошлое, то повторяем расчёт и ухоим в рекурсию
            //if (calcDay < DateTimeOffset.UtcNow /*|| calcDay <= referenceDate*/)
            //    calcDay = GetNextDateAnnually2(values, calcDay.AddYears(1));

            //return calcDay;

            #endregion

            var nowInTargetZone = DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset);

            // Начинаем расчет с года переданной referenceDate
            int yearsToAdd = 0;

            while (true)
            {
                // Сдвигаем проверяемый год относительно исходной даты
                var currentYearDate = referenceDate.AddYears(yearsToAdd);
                int year = currentYearDate.Year;

                // Формируем первое число целевого месяца для проверяемого года
                var startDayOfMonth = new DateTimeOffset(
                    year,
                    monthNum,
                    1,
                    referenceDate.Hour,
                    referenceDate.Minute,
                    referenceDate.Second,
                    referenceDate.Offset
                );

                // Ищем все нужные дни недели в этом месяце
                var daysList = FindAllDaysInMonth(dwNum, startDayOfMonth);

                DateTimeOffset calcDay;

                if (orderNum == 5) // 5 означает "Последний день недели в месяце"
                {
                    calcDay = daysList.Last();
                }
                else
                {
                    // Умная защита: если в этом году в этом месяце НЕТ 5-го выбранного дня недели
                    if (orderNum > daysList.Count)
                    {
                        // Мы не падаем с ошибкой, а просто переходим к проверке следующего года
                        yearsToAdd++;
                        continue;
                    }

                    calcDay = daysList[orderNum - 1];
                }

                if (calcDay >= referenceDate && calcDay >= nowInTargetZone)
                {
                    return calcDay;
                }

                // Если вычисленная дата в прошлом — переходим на следующий год
                yearsToAdd++;
            }
        }


        private List<DateTimeOffset> FindAllDaysInMonth(int dwNum, DateTimeOffset startDay)
        {
            var calcDay = startDay;
            if (((int)calcDay.DayOfWeek) != dwNum)
                calcDay = OffsetToDayOfWeek(dwNum, calcDay);

            var daysList = new List<DateTimeOffset>();
            var monthNum = calcDay.Month;
            while (calcDay.Month == monthNum)
            {
                daysList.Add(calcDay);
                calcDay = calcDay.AddDays(7);
            }

            return daysList;
        }


        private DateTimeOffset OffsetToDayOfWeek(int dwNum, DateTimeOffset calcDay)
        {
            var daysOffset = dwNum - ((int)calcDay.DayOfWeek);

            if (daysOffset < 0)
                daysOffset = 7 + daysOffset;

            return calcDay.AddDays(daysOffset);
        }

    }
}
