using AutoMapper;
using Microsoft.EntityFrameworkCore;
using PARR.Core.Repositories.Interfaces;
using PARR.Core.Repositories.Interfaces.RobotRepositories;
using PARR.Domain.DTOs.RobotSnapshotDTO;
using PARR.Domain.DTOs.User;
using PARR.Domain.Entities.RobotEntities;
using PARR.Domain.Exceptions;

namespace PARR.Core.Services.RobotSnapshotServices
{
    internal class RobotSnapshotService : IRobotSnapshotService
    {
        private readonly IRobotSnapshotRepository _robotSnapshotRepository;
        private readonly IUserRepository _userRepository;
        private readonly IMapper _mapper;

        public RobotSnapshotService(
                IRobotSnapshotRepository robotSnapshotRepository,
                IUserRepository userRepository,
                IMapper mapper
            )
        {
            _robotSnapshotRepository = robotSnapshotRepository;
            _userRepository = userRepository;
            _mapper = mapper;
        }


        public async Task<List<RobotSnapshotItemDto>> GetAsync(RobotSnapshotQuery queryDto)
        {
            // Просто возвращаем данные из БД, не проверяем что есть промежутки между которыми не было данных
            // Т.е это просто чтоб посмотреть что есть в БД

            var minutes = queryDto.Minutes > 0 ? queryDto.Minutes : 60 * 12;

            var dateEnd = DateTimeOffset.UtcNow;
            var dateStart = dateEnd.AddMinutes(-minutes);

            var query = _robotSnapshotRepository.Get().AsNoTracking()
                                                .Where(t => dateStart <= t.DateCreated && t.DateCreated <= dateEnd);

            if (!string.IsNullOrEmpty(queryDto.Ip))
                query = query.Where(t => t.Ip == queryDto.Ip);

            var groupingSnapshots = (await query.ToListAsync()).GroupBy(t => t.Ip);


            var robotIps = groupingSnapshots.Select(t => t.Key).ToHashSet();
            var robotList = await _userRepository.Get()
                                                    .AsNoTracking()
                                                    .Where(t => robotIps.Contains(t.Ip))
                                                    .ToDictionaryAsync(t => t.Ip);

            var result = new List<RobotSnapshotItemDto>();

            foreach (var item in groupingSnapshots)
            {
                robotList.TryGetValue(item.Key, out var robot);

                var userDto = new UserBaseDto { Ip = item.Key, Description = robot?.Description ?? string.Empty, Name = robot?.Name ?? string.Empty };
                var snapshots = _mapper.Map<List<RobotSnapshotDto>>(item).OrderBy(t => t.DateCreated).ToList();

                result.Add(new RobotSnapshotItemDto { Robot = userDto, Snapshots = snapshots });
            }

            return result.OrderBy(t => t.Robot.Name).ThenBy(t => t.Robot.Ip).ToList();
        }


        public async Task<List<ServerHourlyAnalyticsDto>> GetHourlyAnalyticsAsync(RobotAnalyticsQuery queryDto)
        {
            var snapshotsQuery = _robotSnapshotRepository.Get()
                                        .AsNoTracking()
                                        .Where(t => queryDto.DateStart <= t.DateCreated && t.DateCreated <= queryDto.DateEnd);

            // Фильтруем по конкретному IP
            if (!string.IsNullOrEmpty(queryDto.Ip))
                snapshotsQuery = snapshotsQuery.Where(t => t.Ip == queryDto.Ip);


            // Группируем на стороне PostgreSQL с учетом часового пояса пользователя.
            // Метод .Add(offset) сдвигает UTC-дату в Postgres на интервал времени пользователя, 
            // благодаря чему минуты объединяются в правильные локальные "часовые корзины".
            var rawGroupedData = await snapshotsQuery
                                            .GroupBy(t => new
                                            {
                                                t.Ip,
                                                // Явно сдвигаем дату и достаем только нужные компоненты
                                                Year = (t.DateCreated + queryDto.Offset).DateTime.Year,
                                                Month = (t.DateCreated + queryDto.Offset).DateTime.Month,
                                                Day = (t.DateCreated + queryDto.Offset).DateTime.Day,
                                                Hour = (t.DateCreated + queryDto.Offset).DateTime.Hour
                                            })
                                            .Select(g => new
                                            {
                                                g.Key.Ip,
                                                // Вытаскиваем компоненты даты из уже смещенного локального времени
                                                LocalYear = g.Key.Year,
                                                LocalMonth = g.Key.Month,
                                                LocalDay = g.Key.Day,
                                                LocalHour = g.Key.Hour,

                                                MaxAllowed = g.Max(t => t.MaxRobots),
                                                MaxTemplates = g.Max(t => t.TemplateRobotsCount),
                                                MaxSchedules = g.Max(t => t.ScheduleRobotsCount),
                                                SumTemplates = g.Sum(t => t.TemplateRobotsCount),
                                                SumSchedules = g.Sum(t => t.ScheduleRobotsCount)
                                            })
                                            .ToListAsync();

            if (!rawGroupedData.Any())
                return new List<ServerHourlyAnalyticsDto>();

            // Получаем информацию о серверах
            var serverIps = rawGroupedData.Select(g => g.Ip).ToHashSet();
            var serverDictionary = await _userRepository.Get()
                                                .AsNoTracking()
                                                .Where(t => serverIps.Contains(t.Ip))
                                                .ToDictionaryAsync(t => t.Ip);

            // Группируем полученные агрегаты по серверам уже в памяти
            var dataByServers = rawGroupedData.GroupBy(g => g.Ip);
            var result = new List<ServerHourlyAnalyticsDto>();

            // Принудительно переводим UTC-границы запроса в локальное время пользователя для корректной генерации "дыр"
            var localStart = queryDto.DateStart.ToOffset(queryDto.Offset).DateTime;
            var localEnd = queryDto.DateEnd.ToOffset(queryDto.Offset).DateTime;

            // Округляем начальный час до ровного значения (00 минут, 00 секунд)
            var startHour = new DateTime(localStart.Year, localStart.Month, localStart.Day, localStart.Hour, 0, 0);

            foreach (var serverGroup in dataByServers)
            {
                serverDictionary.TryGetValue(serverGroup.Key, out var robotInfo);

                var userDto = new UserBaseDto
                {
                    Ip = serverGroup.Key,
                    Description = robotInfo?.Description ?? string.Empty,
                    Name = robotInfo?.Name ?? string.Empty
                };

                // Формируем точки, которые удалось вытащить из базы данных
                var hourlyPoints = serverGroup.Select(g => new RobotHourlyPointDto
                {
                    // Возвращаем дату со смещением пользователя, чтобы Angular сразу отображал правильный час
                    Hour = new DateTimeOffset(g.LocalYear, g.LocalMonth, g.LocalDay, g.LocalHour, 0, 0, TimeSpan.Zero),
                    MaxAllowedRobots = g.MaxAllowed,
                    MaxTemplateRobots = g.MaxTemplates,
                    MaxScheduleRobots = g.MaxSchedules,
                    // Считаем среднее количество роботов, деля поминутную сумму строго на 60 минут часа
                    //todo: тут не всегда нужно делить на 60, если час последний, то там может быть 50 минут или 20 и тп
                    AvgTemplateRobots = Math.Round(g.SumTemplates / 60.0, 1),
                    AvgScheduleRobots = Math.Round(g.SumSchedules / 60.0, 1)
                })
                .ToList();

                // Алгоритм заполнения пропусков во времени (если сервер был выключен)
                var filledPoints = new List<RobotHourlyPointDto>();
                var currentHour = startHour;
                int lastKnownMax = hourlyPoints.FirstOrDefault()?.MaxAllowedRobots ?? 0;

                while (currentHour <= localEnd)
                {
                    // Создаем временную точку со смещением для точного сопоставления
                    var targetOffsetDateTime = new DateTimeOffset(currentHour, TimeSpan.Zero);
                    var point = hourlyPoints.FirstOrDefault(p => p.Hour == targetOffsetDateTime);

                    if (point != null)
                    {
                        filledPoints.Add(point);
                        lastKnownMax = point.MaxAllowedRobots;
                    }
                    else
                    {
                        // Если за этот час записей в БД нет — значит, сервер был полностью оффлайн.
                        // Выводим нули, чтобы график в Angular не прерывался, а плавно падал.
                        filledPoints.Add(new RobotHourlyPointDto
                        {
                            Hour = targetOffsetDateTime,
                            MaxAllowedRobots = lastKnownMax,
                            MaxTemplateRobots = 0,
                            AvgTemplateRobots = 0,
                            MaxScheduleRobots = 0,
                            AvgScheduleRobots = 0
                        });
                    }
                    // Шагаем строго на +1 час вперед по локальному времени клиента
                    currentHour = currentHour.AddHours(1);
                }

                result.Add(new ServerHourlyAnalyticsDto
                {
                    Robot = userDto,
                    Snapshots = filledPoints.OrderBy(p => p.Hour).ToList()
                });
            }

            return result
                   .OrderBy(t => t.Robot.Name)
                   .ThenBy(t => t.Robot.Ip)
                   .ToList();
        }


        public async Task<List<HourlyRobotTypeAnalyticsDto>> GetHourlyAnalyticsByRobotTypeAsync(RobotAnalyticsRobotTypeQuery queryDto)
        {
            var snapshotsQuery = _robotSnapshotRepository.Get()
                                       .AsNoTracking()
                                       .Where(t => queryDto.DateStart <= t.DateCreated && t.DateCreated <= queryDto.DateEnd)
                                       .GroupBy(t => new
                                       {
                                           // Явно сдвигаем дату
                                           Year = (t.DateCreated + queryDto.Offset).DateTime.Year,
                                           Month = (t.DateCreated + queryDto.Offset).DateTime.Month,
                                           Day = (t.DateCreated + queryDto.Offset).DateTime.Day,
                                           Hour = (t.DateCreated + queryDto.Offset).DateTime.Hour
                                       });

            IQueryable<RobotSnapshotRobotTypeDbDto> groupedQuery;

            switch (queryDto.RobotType)
            {
                case Domain.Enums.RobotsEnum.TemplateOrder:
                    groupedQuery = snapshotsQuery.Select(g => new RobotSnapshotRobotTypeDbDto
                    {
                        // Вытаскиваем компоненты даты из уже смещенного локального времени
                        LocalYear = g.Key.Year,
                        LocalMonth = g.Key.Month,
                        LocalDay = g.Key.Day,
                        LocalHour = g.Key.Hour,

                        // Макс не правильно. 
                        //MaxAllowed = g.Max(t => t.MaxRobots),
                        Sum = g.Sum(t => t.TemplateRobotsCount)
                    });
                    break;
                case Domain.Enums.RobotsEnum.ScheduleOrder:
                    groupedQuery = snapshotsQuery.Select(g => new RobotSnapshotRobotTypeDbDto
                    {
                        // Вытаскиваем компоненты даты из уже смещенного локального времени
                        LocalYear = g.Key.Year,
                        LocalMonth = g.Key.Month,
                        LocalDay = g.Key.Day,
                        LocalHour = g.Key.Hour,

                        // Макс не правильно. 
                        //MaxAllowed = g.Max(t => t.MaxRobots),
                        Sum = g.Sum(t => t.ScheduleRobotsCount)
                    });
                    break;
                default:
                    throw new ArgumentException("Не обработано значение параметра", nameof(queryDto.RobotType));
            }

            var data = await groupedQuery.ToListAsync();

            if (!data.Any())
                return new List<HourlyRobotTypeAnalyticsDto>();

            // Фиксируем текущее время в UTC и «округляем» его до начала часа
            var nowLocal = DateTimeOffset.UtcNow.ToOffset(queryDto.Offset);
            var currentHourLocal = new DateTime(nowLocal.Year, nowLocal.Month, nowLocal.Day, nowLocal.Hour, 0, 0);

            var dbDataDict = data.ToDictionary(
                k => new DateTime(k.LocalYear, k.LocalMonth, k.LocalDay, k.LocalHour, 0, 0),
                v =>
                {
                    double minutesInHour = 60.0;
                    var dbLocalHour = new DateTime(v.LocalYear, v.LocalMonth, v.LocalDay, v.LocalHour, 0, 0);
                    // Строгая проверка: совпадает ли час из базы данных ИМЕННО с текущим часом сегодня?
                    // Если да, то нужно делить не на 60, а на прошедние минуты
                    if (dbLocalHour == currentHourLocal)
                    {
                        // Если идет первая минута часа, делим на 1, чтобы избежать деления на ноль
                        minutesInHour = nowLocal.Minute == 0 ? 1.0 : (double)nowLocal.Minute;
                    }

                    return new
                    {
                        //MaxAllowedRobots = t.MaxAllowed,
                        AvgRobots = Math.Round(v.Sum / minutesInHour, 1)
                    };
                });

            // Заполняем модель
            var statistics = new List<HourlyRobotTypeAnalyticsDto>();

            #region Алгоритм заполнения пропусков во времени (если сервер был выключен)
            // Превращаем границы в локальное время для генерации сетки часов
            var currentHour = queryDto.DateStart.ToOffset(queryDto.Offset).DateTime;
            var localEnd = queryDto.DateEnd.ToOffset(queryDto.Offset).DateTime;

            // Срезаем минуты/секунды для старта
            currentHour = new DateTime(currentHour.Year, currentHour.Month, currentHour.Day, currentHour.Hour, 0, 0);

            while (currentHour <= localEnd)
            {
                // Ищем значение в словаре. Если нет — ставим 0
                dbDataDict.TryGetValue(currentHour, out var statInfo);

                // 1. Создаем DateTimeOffset для локального времени с правильным смещением
                var localDateTimeOffset = new DateTimeOffset(currentHour, queryDto.Offset);

                statistics.Add(new HourlyRobotTypeAnalyticsDto
                {
                    // Возвращаем DateTimeOffset с нулевым смещением (как в вашем коде) или с queryDto.Offset
                    Hour = localDateTimeOffset.ToUniversalTime(),
                    AvgRobots = statInfo?.AvgRobots ?? 0
                });

                currentHour = currentHour.AddHours(1);
            }
            #endregion

            return statistics;
        }


        public async Task<RobotSnapshotItemDto> CreateAsync(CreateRobotSnapshot robotSnapshot)
        {
            var snapshot = new RobotSnapshot
            {
                Id = Guid.NewGuid(),
                TemplateRobotsCount = robotSnapshot.TemplateRobotsCount,
                ScheduleRobotsCount = robotSnapshot.ScheduleRobotsCount,
                MaxRobots = robotSnapshot.MaxRobots,
                Ip = robotSnapshot.Ip
            };

            var createdResult = await _robotSnapshotRepository.CreateAsync(snapshot);
            var commitResult = await _robotSnapshotRepository.CommitAsync();

            if (!createdResult || !commitResult)
                throw new DbErrorException("Ошибка сохранения в БД");

            var user = await _userRepository.Get().AsNoTracking().FirstOrDefaultAsync(t => t.Ip == robotSnapshot.Ip);
            var userDto = new UserBaseDto { Ip = robotSnapshot.Ip, Description = user?.Description ?? string.Empty, Name = user?.Name ?? string.Empty };

            var result = new RobotSnapshotItemDto
            {
                Robot = userDto,
                Snapshots = _mapper.Map<List<RobotSnapshotDto>>(new List<RobotSnapshot> { snapshot })
            };

            return result;
        }


    }
}
