using Microsoft.Extensions.Logging;
using PARR.Constants;
using PARR.DAL.Contracts;
using PARR.DAL.DomainModels;
using PARR.DAL.Extensions;
using PARR.DAL.Services.Interfaces;

namespace PARR.DAL.TransformServices
{
    internal class EsppScheduleTransformService : IEsppScheduleTransformService
    {
        private readonly IEsppSchTypeConfigService esppSchTypeConfigService;
        private readonly INextRunModifierService nextRunModifierService;
        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,
            IEsppSchTypeConfigService esppSchTypeConfigService,
            INextRunModifierService nextRunModifierService
            )
        {
            this.esppSchTypeConfigService = esppSchTypeConfigService;
            this.nextRunModifierService = nextRunModifierService;
            this.logger = logger;
        }

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

            return GetNextDate(esppSchedule, referenceDate);
        }

        public DateTimeOffset GetNextDate(EsppScheduleDto esppSchedule, DateTimeOffset referenceDate)
        {
            var nextRun = referenceDate;

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

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

            // Возвращаем обратно в ЮТС
            nextRun = nextRun.ToOffset(TimeSpan.Zero);
            //возможно для автораспределения нужно nextRun = nextRunModifierService.GetWorkDayAsync(nextRun).GetAwaiter().GetResult();

            // тут может быть 21 час по МСК, то это предыдущие сутки, нужно отнять 1 день
            //if (!DateResolver.IsCurrentDayRelativeMskTime(nextRun.Hour))
            //    nextRun = nextRun.AddDays(-1);

            return nextRun;
        }


        private List<DateTimeOffset> GetNextSchedule(EsppScheduleDto esppSchedule, DateTimeOffset referenceDate)
        {
            // расписание на оставшееся на сегодня время. его так мало...
            referenceDate = referenceDate.UtcDateTime;
            var schedules = new List<DateTimeOffset>();
            var curTime = referenceDate;

            //Если ещё не произошло то добавляем расписание, возможно это новое AiW и он ещё ниразу не запускался.
            //lastRun это будущее
            if (referenceDate >= DateTimeOffset.UtcNow && referenceDate <= DateTimeOffset.UtcNow.EndOfDay())
                schedules.Add(referenceDate);

            while (curTime < DateTimeOffset.UtcNow.EndOfDay())
            {
                switch (esppSchedule.TypeSchedule.Id)
                {
                    case (int)EsppSchTypeScheduleEnum.Regularly:
                        curTime = GetNextDateRegularly(esppSchedule.Values, curTime);
                        break;
                    case (int)EsppSchTypeScheduleEnum.Weekly:
                        curTime = GetNextDateWeekly(esppSchedule.Values, curTime);
                        break;
                    case (int)EsppSchTypeScheduleEnum.Monthly:
                        curTime = GetNextDateMonthly(esppSchedule.Values, curTime);
                        break;
                    case (int)EsppSchTypeScheduleEnum.Monthly2:
                        curTime = GetNextDateMonthly2(esppSchedule.Values, curTime);
                        break;
                    case (int)EsppSchTypeScheduleEnum.Annually:
                        curTime = GetNextDateAnnually(esppSchedule.Values, curTime);
                        break;
                    case (int)EsppSchTypeScheduleEnum.Annually2:
                        curTime = GetNextDateAnnually2(esppSchedule.Values, curTime);
                        break;
                }

                schedules.Add(curTime);
            }
            schedules.RemoveAll(s => s > DateTimeOffset.UtcNow.EndOfDay());

            return schedules.OrderBy(t => t).ToList();
        }





        public async Task<List<DateTimeOffset>> GetNextScheduleAsync(Guid jobGroupId, DateTimeOffset referenceDate)
        {
            //TODO: тут не проверен переход через выходные дни!!! Переход не используется, так как тут не рассчитываем NextRun.
            //В общем проверить, когда будем тестировать агента, что с датами все ок
            var esppSchedule = await GetEsppScheduleAsync(jobGroupId);
            var nextSchedule = GetNextSchedule(esppSchedule, referenceDate);

            return nextSchedule;
        }


        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)
                calcDay = new DateTimeOffset(DateTimeOffset.UtcNow.Year, DateTimeOffset.UtcNow.Month, 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)
                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 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 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;

            if (year == DateTimeOffset.UtcNow.Year && referenceDate.Month < DateTimeOffset.UtcNow.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);
        }


        public DateTimeOffset GetNextDateForDistributionRun(DateTimeOffset lastRun, DistributionPeriodTypeEnum periodType, string distributionPeriod)
        {
            var nextRun = lastRun;

            switch (periodType)
            {
                case (DistributionPeriodTypeEnum.Day):
                    nextRun = lastRun.AddDays(ParseInt(distributionPeriod));
                    break;
                case (DistributionPeriodTypeEnum.Month):
                    nextRun = lastRun.AddMonths(ParseInt(distributionPeriod));
                    break;
                case (DistributionPeriodTypeEnum.Year):
                    nextRun = lastRun.AddYears(ParseInt(distributionPeriod));
                    break;
            }

            return nextRunModifierService.GetWorkDayAsync(nextRun).GetAwaiter().GetResult();
        }


        private DateTimeOffset GetPrevDateForDistributionRun(DateTimeOffset lastRun, DistributionPeriodTypeEnum periodType, string distributionPeriod)
        {
            switch (periodType)
            {
                case (DistributionPeriodTypeEnum.Day):
                    return lastRun.AddDays(-ParseInt(distributionPeriod));

                case (DistributionPeriodTypeEnum.Month):
                    return lastRun.AddMonths(-ParseInt(distributionPeriod));

                case (DistributionPeriodTypeEnum.Year):
                    return lastRun.AddYears(-ParseInt(distributionPeriod));
            }

            return lastRun;
        }


        public async Task<DateOnly> GetStartPeriodForDateAsync(Guid jobGroupId, DateTimeOffset date, DateTimeOffset referenceDate, DistributionPeriodTypeEnum periodType, string distributionPeriod)
        {
            var esppSchedule = await GetEsppScheduleAsync(jobGroupId);

            var currentStartPeriod = referenceDate;
            var currentEndPeriod = GetNextDateForDistributionRun(currentStartPeriod, periodType, distributionPeriod);

            while (!(date >= currentStartPeriod && date < currentEndPeriod))
            {
                if (date > currentEndPeriod)
                {
                    currentStartPeriod = currentEndPeriod;
                    currentEndPeriod = GetNextDateForDistributionRun(currentStartPeriod, periodType, distributionPeriod);
                }
                else
                {
                    currentEndPeriod = currentStartPeriod;
                    currentStartPeriod = GetPrevDateForDistributionRun(currentStartPeriod, periodType, distributionPeriod);
                }
            }

            return DateOnly.FromDateTime(currentStartPeriod.DateTime);
        }


        private int ParseInt(string value)
        {
            try
            {
                return int.Parse(value);
            }
            catch (Exception ex)
            {
                logger.LogError(ex, $"{nameof(this.GetType)}, получение конца периода. Не смог распарсить string в int для значения {value}.");
                return 0;
            }
        }


        private DistributionPeriodTypeEnum ParseDistributionPeriodType(string value)
        {
            var result = (DistributionPeriodTypeEnum)Enum.Parse(typeof(DistributionPeriodTypeEnum), value);

            return result;
        }

    }
}
