using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using PARR.Core.Common.Interfaces;
using PARR.Core.Repositories.Interfaces.Unit;
using PARR.Domain.Cache;
using PARR.Domain.DTOs.UnitDto;

namespace PARR.Core.Services.UnitService.Implementations
{
    /// <summary>
    /// Сервис по работе с юнитами из кэш и бд
    /// </summary>
    internal class UnitCacheService
    {
        /// <summary>
        /// TTL для кэш
        /// </summary>
        private readonly TimeSpan CacheTtl = TimeSpan.FromHours(24);

        /// <summary>
        /// Размер порции данных, для запроса в БД
        /// </summary>
        private readonly int DatabaseBatchSize = 500;

        private readonly ILogger<UnitCacheService> logger;
        private readonly IUnitRepository unitRepository;
        private readonly IRedisCacheService redisCacheService;

        public UnitCacheService(
                ILogger<UnitCacheService> logger,
                IUnitRepository unitRepository,
                IRedisCacheService redisCacheService
            )
        {
            this.logger = logger;
            this.unitRepository = unitRepository;
            this.redisCacheService = redisCacheService;
        }

        /// <summary>
        /// Получить UnitInfo по id
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public async Task<UnitInfo?> GetAsync(Guid id)
        {
            var hashKey = redisCacheService.GetKey(CacheKeys.Unit.UnitHashWithBucket(id));
            var unitKey = redisCacheService.GetKey(CacheKeys.Unit.UnitItem(id));

            // Смотрим, есть ли в кэш
            var cache = await redisCacheService.GetHashFieldAsync<UnitInfo?>(hashKey, unitKey, true);
            if (cache != null)
            {
                logger.LogDebug("Данные о юните {UnitId} получены из кэша.", id);
                return cache;
            }

            // Нет в кэш, ищем в бд
            var dbData = await GetUnitAsync(id);
            if (dbData == null)
            {
                logger.LogWarning("В БД нет данных о юните {UnitId}. Вернулся null", id);
                return null;
            }

            // Сохраняем в кэш
            await redisCacheService.SetHashFieldAsync(hashKey, unitKey, dbData, CacheTtl, true);

            logger.LogDebug("Вернули юнит полученный из БД  {UnitId}.", id);

            return dbData;
        }

        /// <summary>
        /// Получить словарь UnitInfo
        /// </summary>
        /// <param name="ids"></param>
        /// <returns></returns>
        public async Task<Dictionary<Guid, UnitInfo>> GetAsync(IReadOnlySet<Guid> ids)
        {
            if (ids == null || ids.Count == 0)
                return new Dictionary<Guid, UnitInfo>();

            // Формируем ключи
            var keys = ids.Select(t =>
            {
                var (hashKey, fieldKey) = (redisCacheService.GetKey(CacheKeys.Unit.UnitHashWithBucket(t)), redisCacheService.GetKey(CacheKeys.Unit.UnitItem(t)));
                return (HashKey: hashKey, FieldKey: fieldKey);
            }).ToList();

            logger.LogDebug("Необходимо найти инорфмацию для {Count} объектов.", ids.Count);

            // Получаем из кэш
            var cacheData = await redisCacheService.GetHashFieldsAsync<UnitInfo>(keys, true);

            logger.LogDebug("Из кэша получена информация о {Count} объектах.", cacheData.Count);

            var resultDictionary = cacheData.ToDictionary(unit => unit.Id, unit => unit);

            // Проверям каких данных нет в кэш, догрузим из из БД
            if (resultDictionary.Count < ids.Count)
            {
                // Находим id которых нет в кэше
                var missingIds = ids.Where(id => !resultDictionary.ContainsKey(id)).ToHashSet();

                logger.LogDebug("Запрашиваем из БД дополнительно {Count} юнитов", missingIds.Count);

                var dbData = await GetUnitsAsync(missingIds);

                // список объектов для сохранения в кэш
                var dataToCache = new List<(string HashKey, string FieldKey, UnitInfo Value)>();

                foreach (var unit in dbData)
                {
                    resultDictionary[unit.Id] = unit;

                    // Добавляем в кэш
                    var hashKey = redisCacheService.GetKey(CacheKeys.Unit.UnitHashWithBucket(unit.Id));
                    var unitKey = redisCacheService.GetKey(CacheKeys.Unit.UnitItem(unit.Id));

                    //await redisCacheService.SetHashFieldAsync(hashKey, unitKey, unit, CacheTtl, true);
                    dataToCache.Add((hashKey, unitKey, unit));
                }

                if (dataToCache.Count > 0)
                    await redisCacheService.SetHashFieldsAsync<UnitInfo>(dataToCache, CacheTtl, true);
            }

            return resultDictionary;
        }

        /// <summary>
        /// Удалить запись UnitInfo из кэш
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        /// <exception cref="NotImplementedException"></exception>
        public async Task RemoveFromCacheAsync(Guid id)
        {
            var hashKey = redisCacheService.GetKey(CacheKeys.Unit.UnitHashWithBucket(id));
            var unitKey = redisCacheService.GetKey(CacheKeys.Unit.UnitItem(id));

            await redisCacheService.DeleteHashFieldAsync(hashKey, unitKey);
        }

        /// <summary>
        /// Удалить записи UnitInfo из кэш
        /// </summary>
        /// <param name="ids"></param>
        /// <returns></returns>
        public async Task RemoveFromCacheAsync(IReadOnlySet<Guid> ids)
        {
            if (ids == null || ids.Count == 0)
                return;

            // Формируем ключи
            var keys = ids.Select(t =>
            {
                var (hashKey, fieldKey) = (redisCacheService.GetKey(CacheKeys.Unit.UnitHashWithBucket(t)), redisCacheService.GetKey(CacheKeys.Unit.UnitItem(t)));
                return (HashKey: hashKey, FieldKey: fieldKey);
            }).ToList();

            await redisCacheService.DeleteHashFieldsAsync(keys);
        }


        /// <summary>
        /// Получить значение по одному юниту из БД
        /// </summary>
        /// <param name="unitId"></param>
        /// <returns></returns>
        private async Task<UnitInfo?> GetUnitAsync(Guid unitId)
        {
            var unit = await unitRepository.Get()
                                .AsNoTracking()
                                .Select(t => new
                                {
                                    Id = t.Id,
                                    Name = t.Name,
                                    Values = t.UnitValues.Select(x => new { FieldId = x.FieldId, Value = x.Value!.Value }),
                                    ParentUnits = t.ParentUnits.Select(t => t.ParentUnitId),
                                    ChildUnits = t.ChildUnits.Select(t => t.ChildUnitId)
                                })
                                .FirstOrDefaultAsync(t => t.Id == unitId);

            if (unit == null)
                return null;

            // Склеим вместе родителей и детей, чтоб не делать лишних запросов к бд
            var relatives = unit.ParentUnits.Union(unit.ChildUnits).ToList();

            // Получаем базовую инфу о родственниках
            var relativesDictionary = await unitRepository.Get()
                                        .AsNoTracking()
                                        .Where(t => relatives.Contains(t.Id))
                                        .Select(t => new UnitInfoBase
                                        {
                                            Id = t.Id,
                                            Name = t.Name,
                                            Values = t.UnitValues.Select(x => new UnitInfoAttribute
                                            {
                                                FieldId = x.FieldId,
                                                Value = x.Value!.Value
                                            }).ToList()
                                        }).ToDictionaryAsync(t => t.Id);

            // Собираем итоговый UnitInfo
            return new UnitInfo
            {
                Id = unit.Id,
                Name = unit.Name,
                Values = unit.Values.Select(x => new UnitInfoAttribute { FieldId = x.FieldId, Value = x.Value }).ToList(),
                ParentUnits = unit.ParentUnits
                                    .Where(id => relativesDictionary.ContainsKey(id))
                                    .Select(id => relativesDictionary[id])
                                    .ToList(),
                ChildUnits = unit.ChildUnits
                                    .Where(id => relativesDictionary.ContainsKey(id))
                                    .Select(id => relativesDictionary[id])
                                    .ToList()
            };
        }

        /// <summary>
        /// Получить значения по списку юнитов
        /// </summary>
        /// <param name="ids"></param>
        /// <returns></returns>
        private async Task<List<UnitInfo>> GetUnitsAsync(IReadOnlySet<Guid> ids)
        {
            if (ids == null || ids.Count == 0)
                return new List<UnitInfo>();

            // Пакетно получаем основные данные для юнитов
            var dbUnits = new List<dynamic>();

            // Выполняем порционно
            foreach (var chank in ids.Chunk(DatabaseBatchSize))
            {
                var chunkUnits = await unitRepository.Get()
                                            .AsNoTracking()
                                            .Where(t => chank.Contains(t.Id))
                                            .Select(t => new
                                            {
                                                Id = t.Id,
                                                Name = t.Name,
                                                Values = t.UnitValues.Select(x => new { FieldId = x.FieldId, Value = x.Value!.Value }).ToList(),
                                                ParentUnits = t.ParentUnits.Select(p => p.ParentUnitId).ToList(),
                                                ChildUnits = t.ChildUnits.Select(c => c.ChildUnitId).ToList()
                                            })
                                            .ToListAsync();

                dbUnits.AddRange(chunkUnits);
            }

            if (dbUnits.Count == 0)
                return new List<UnitInfo>();

            // Собираем ВСЕ ID родственников(и родителей, и детей) для ВСЕХ найденных юнитов в один HashSet
            var allRelativeIds = new HashSet<Guid>();
            foreach (var unit in dbUnits)
            {
                foreach (var parentId in unit.ParentUnits)
                    allRelativeIds.Add(parentId);

                foreach (var childId in unit.ChildUnits)
                    allRelativeIds.Add(childId);
            }

            // Пакетно загружаем базовую информацию обо всех родственниках за один раз
            var relativesDictionary = new Dictionary<Guid, UnitInfoBase>();

            foreach (var chunk in allRelativeIds.Chunk(DatabaseBatchSize))
            {
                var chunkRelatives = await unitRepository.Get()
                                            .AsNoTracking()
                                            .Where(t => chunk.Contains(t.Id))
                                            .Select(t => new UnitInfoBase
                                            {
                                                Id = t.Id,
                                                Name = t.Name,
                                                Values = t.UnitValues.Select(x => new UnitInfoAttribute
                                                {
                                                    FieldId = x.FieldId,
                                                    Value = x.Value!.Value
                                                }).ToList()
                                            })
                                            .ToDictionaryAsync(t => t.Id);

                foreach (var kvp in chunkRelatives)
                {
                    relativesDictionary[kvp.Key] = kvp.Value;
                }
            }

            // Собираем итоговый список UnitInfo, маппим родственников из словаря
            var result = new List<UnitInfo>(dbUnits.Count);
            foreach (var unit in dbUnits)
            {
                var unitInfo = new UnitInfo
                {
                    Id = unit.Id,
                    Name = unit.Name,
                    Values = ((IEnumerable<dynamic>)unit.Values)
                        .Select(x => new UnitInfoAttribute { FieldId = x.FieldId, Value = x.Value })
                        .ToList(),

                    ParentUnits = ((IEnumerable<Guid>)unit.ParentUnits)
                        .Where(id => relativesDictionary.ContainsKey(id))
                        .Select(id => relativesDictionary[id])
                        .ToList(),

                    ChildUnits = ((IEnumerable<Guid>)unit.ChildUnits)
                        .Where(id => relativesDictionary.ContainsKey(id))
                        .Select(id => relativesDictionary[id])
                        .ToList()
                };

                result.Add(unitInfo);
            }

            logger.LogDebug("Получено из БД данных для {Count} юнитов", result.Count);

            return result;
        }

    }
}
