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 = new TimeSpan(3, 0, 0) + (offset ?? TimeSpan.Zero);
            var finalOffset = offset ?? TimeSpan.Zero;

            var offsetReferenceDate = referenceDate.ToOffset(finalOffset);

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

            ////---------
            //// Для рассчета по МСК времени, потому что в ЮТС может быть еще ВСК, а по МСК это уже ПНД
            //var offsetReferenceDate = referenceDate.ToOffset(new TimeSpan(3, 0, 0));
            ////---------

            //// может быть offset ,это работает когда смещение для УЗ ЕСПП и применение ЗО РГ
            //if (offset.HasValue)
            //    offsetReferenceDate = offsetReferenceDate.Off(offsetReferenceDate.Offset + offset);
            //    //offsetReferenceDate = offsetReferenceDate.Add(offset.Value);


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

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

        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},

            };


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

            var calcDay = referenceDate;

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

            return calcDay;
        }


        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}");
            }

            var calcDay = referenceDate;
            if (calcDay < DateTimeOffset.UtcNow)
            {
                var nowInTargetTz = DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset);
                calcDay = new DateTimeOffset(
                                                nowInTargetTz.Year,
                                               //DateTimeOffset.UtcNow.Year,
                                               nowInTargetTz.Month,
                                               //DateTimeOffset.UtcNow.Month,
                                               nowInTargetTz.Day,
                                               //DateTimeOffset.UtcNow.Day,
                                               referenceDate.Hour,
                                               referenceDate.Minute,
                                               referenceDate.Second,
                                               referenceDate.Offset
                                           );
            }


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

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

            return calcDay;
        }


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

            var calcDay = new DateTimeOffset(referenceDate.Year, referenceDate.Month, dayNum, 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,
                                                dayNum,
                                                referenceDate.Hour,
                                                referenceDate.Minute,
                                                referenceDate.Second,
                                                referenceDate.Offset
                                            );
                //calcDay = new DateTimeOffset(DateTimeOffset.UtcNow.Year, DateTimeOffset.UtcNow.Month, dayNum, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset);
            }


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

            return calcDay;
        }


        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}");
            }

            //var startDay = referenceDate < DateTimeOffset.UtcNow
            //    ? new DateTimeOffset(DateTimeOffset.UtcNow.Year, DateTimeOffset.UtcNow.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 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)
                return GetNextDateMonthly2(values, startDay.AddMonths(1));

            return calcDay;
        }


        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}");
            }

            //var year = (referenceDate.Year < DateTimeOffset.UtcNow.Year) ? DateTimeOffset.UtcNow.Year : referenceDate.Year;
            //сравниваем года в одном часовом поясе
            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);

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

            return calcDay;
        }


        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}");
            }

            //var year = (referenceDate.Year < DateTimeOffset.UtcNow.Year) ? DateTimeOffset.UtcNow.Year : referenceDate.Year;
            // сравниваем года в одном часовом поясе
            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];

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

            return calcDay;
        }


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

    }
}
