using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using PARR.Core.Common.Interfaces;
using PARR.Core.Repositories.Interfaces.Job;
using PARR.Core.Repositories.Interfaces.Unit;
using PARR.Core.Services.UnitFilterService.Models;
using PARR.Domain.Cache.Models;
using PARR.Domain.Entities.Job;
using PARR.Domain.Entities.Unit;
using PARR.Domain.Enums;
using System.Diagnostics;

namespace PARR.Core.Services.UnitFilterService;

internal class UnitFilterService : IUnitFilterService
{
#if DEBUG
    private readonly Guid debugTargetUnitId = Guid.Parse("a3b5f3e2-928e-48b7-b481-df4556e1ed32");
#endif

    private const int DebugMaxUnitsToLog = 10;
    private const int DebugMaxRelationsToLog = 5;

    private readonly int batchSize;

    private readonly ILogger<UnitFilterService> logger;
    private readonly IJobRepository jobService;
    private readonly IUnitRepository unitService;
    private readonly IUnitInUnitRepository unitInUnitService;
    private readonly IRedisCacheService cacheService;
    private readonly IUnitFieldRepository unitFieldService;
    private readonly IUnitInValueRepository unitInValueService;

    public UnitFilterService(
        ILogger<UnitFilterService> logger,
        IJobRepository jobService,
        IUnitRepository unitService,
        IUnitInUnitRepository unitInUnitService,
        IUnitInValueRepository unitInValueService,
        IRedisCacheService cacheService,
        IOptions<UnitFilterServiceOptions> options,
        IUnitFieldRepository unitFieldService
        )
    {
        this.logger = logger;
        this.jobService = jobService;
        this.unitService = unitService;
        this.unitInUnitService = unitInUnitService;
        this.cacheService = cacheService;
        this.unitFieldService = unitFieldService;
        this.unitInValueService = unitInValueService;

        batchSize = options.Value.LoadBatchSize;
    }


    public async Task<IEnumerable<UnitFilterResultDto>?> GetUnitsByJobFilterAsync(
        Guid jobId,
        int? takeCount = null,
        CancellationToken cancellationToken = default
        )
    {
        var job = await LoadJobWithFiltersAsync(jobId, cancellationToken);
        return job == null ? null : await GetUnitsByJobFilterAsync(job, takeCount, cancellationToken);
    }


    public async Task<IEnumerable<Guid>?> GetUnitsIdByJobFilterAsync(
        Job job,
        int? takeCount = null,
        CancellationToken cancellationToken = default
        )
    {
        var filteredUnits = await GetUnitsByJobFilterAsync(job, takeCount, cancellationToken);
        return filteredUnits?.Select(u => u.Id).ToList();
    }


    public async Task<IEnumerable<UnitFilterResultDto>?> GetUnitsByJobFilterAsync(
        Job job,
        int? takeCount = null,
        CancellationToken cancellationToken = default
        )
    {
        if (job.Group == null)
            throw new ArgumentNullException(nameof(job.Group), $"Job {job.Id} не содержит Group");
        if (job.Group.GroupType == null)
            throw new ArgumentNullException(nameof(job.Group.GroupType), $"Job {job.Id} не содержит GroupType");
        if (job.UnitFilters == null || !job.UnitFilters.Any())
            throw new ArgumentNullException(nameof(job.UnitFilters), $"Job {job.Id} не содержит UnitFilters");

        var totalStopwatch = Stopwatch.StartNew();
        logger.LogInformation("Начало фильтрации юнитов для Job {JobId} с {FilterCount} фильтрами", job.Id, job.UnitFilters.Count);

        // Собираем все контексты юнитов, прошедших фильтрацию
        var allFilteredContexts = new List<UnitFilterMatchResult>();

        // Преобразуем в список для индексации
        var unitFiltersList = job.UnitFilters.ToList();

        // Этап 1: Применение основных фильтров (Field, Relationship) на уровне SQL
        for (int i = 0; i < unitFiltersList.Count; i++)
        {
            var filter = unitFiltersList[i];
            var filterStopwatch = Stopwatch.StartNew();

            try
            {
                logger.LogDebug("Применение фильтра #{Index} (Id={FilterId})", i + 1, filter.Id);

                // 1. Найти ID юнитов по UnitFilter (Name LIKE)
                var initialUnitIds = await GetUnitIdsFromCacheOrDbAsync(filter, cancellationToken);
                if (!initialUnitIds.Any())
                {
                    logger.LogDebug("Фильтр #{Index}: пропущен (0 юнитов)", i + 1);
                    continue;
                }

#if DEBUG
                if (initialUnitIds.Contains(debugTargetUnitId))
                {
                    logger.LogDebug("DEBUG: Юнит {TargetUnitId} найден в initialUnitIds для фильтра #{Index}", debugTargetUnitId, i + 1);
                }
#endif

                // 2. Применить FieldFilters на уровне SQL
                var fieldStopwatch = Stopwatch.StartNew();
                var fieldFilteredIds = await ApplyFieldFiltersOnDbAsync(initialUnitIds, filter.FieldFilters, cancellationToken);
                fieldStopwatch.Stop();

                if (!fieldFilteredIds.Any())
                {
                    logger.LogDebug("Фильтр #{Index}: 0 юнитов после FieldFilters", i + 1);
                    continue;
                }

#if DEBUG
                if (initialUnitIds.Contains(debugTargetUnitId) && !fieldFilteredIds.Contains(debugTargetUnitId))
                {
                    logger.LogDebug("DEBUG: Юнит {TargetUnitId} ОТФИЛЬТРОВАН на этапе FieldFilters (Фильтр #{Index})", debugTargetUnitId, i + 1);
                }
#endif

                // 3. Применить RelationshipFilters на уровне SQL -> ВОЗВРАЩАЕТ UnitFilterMatchResult
                var relStopwatch = Stopwatch.StartNew();
                var relationshipFilteredContexts = await ProcessRelationshipFiltersAsync(fieldFilteredIds, filter.RelationshipFilters, cancellationToken);
                relStopwatch.Stop();

                if (!relationshipFilteredContexts.Any())
                {
                    logger.LogDebug("Фильтр #{Index}: 0 юнитов после RelationshipFilters", i + 1);
                    continue;
                }

#if DEBUG
                var targetContext = relationshipFilteredContexts.FirstOrDefault(c => c.UnitId == debugTargetUnitId);
                if (targetContext != null)
                {
                    logger.LogDebug("DEBUG: Юнит {TargetUnitId} прошёл фильтр #{Index}. Родителей: {ParentCount}, Детей: {ChildCount}",
                        debugTargetUnitId, i + 1, targetContext.ValidParentIds.Count, targetContext.ValidChildIds.Count);
                }
#endif

                // Добавляем отфильтрованные контексты в общий набор
                allFilteredContexts.AddRange(relationshipFilteredContexts);

                filterStopwatch.Stop();
                logger.LogDebug(
                    "Фильтр #{Index}: добавлено {Count} юнитов. Всего: {Total}. [Field: {F}ms, Rel: {R}ms, Total: {T}ms]",
                    i + 1,
                    relationshipFilteredContexts.Count,
                    allFilteredContexts.Count,
                    fieldStopwatch.ElapsedMilliseconds,
                    relStopwatch.ElapsedMilliseconds,
                    filterStopwatch.ElapsedMilliseconds
                );
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "Ошибка при обработке фильтра {FilterId} для Job {JobId}",
                    filter.Id, job.Id);
            }
        }

        // Убираем дубликаты по UnitId, объединяя связи
        var mergedContexts = allFilteredContexts
            .GroupBy(c => c.UnitId)
            .Select(g => new UnitFilterMatchResult
            {
                UnitId = g.Key,
                ValidParentIds = new HashSet<Guid>(g.SelectMany(c => c.ValidParentIds)),
                ValidChildIds = new HashSet<Guid>(g.SelectMany(c => c.ValidChildIds))
            })
            .ToList();

        logger.LogInformation("Этап базовой фильтрации завершён: собрано {UnitCount} уникальных юнитов", mergedContexts.Count);

        // Этап 2: Применение Umbrella-фильтра
        var umbrellaFilteredContexts = mergedContexts;
        if (job.Group.GroupType.Code == JobGroupTypesEnum.Umbrella)
        {
            logger.LogDebug("Применяется Umbrella-фильтр: Min={Min}, Max={Max}, IsParent={IsParent}",
                job.MinValueRelationships, job.MaxValueRelationships, job.IsParentRelationships);

#if DEBUG
            var targetBeforeUmbrella = mergedContexts.FirstOrDefault(c => c.UnitId == debugTargetUnitId);
            if (targetBeforeUmbrella != null)
            {
                var count = job.IsParentRelationships == true
                    ? targetBeforeUmbrella.ValidParentIds.Count
                    : targetBeforeUmbrella.ValidChildIds.Count;
                var passes = count >= (job.MinValueRelationships ?? 0) && count <= (job.MaxValueRelationships ?? int.MaxValue);

                logger.LogDebug("DEBUG: Юнит {TargetUnitId} перед Umbrella: Count={Count}, Min={Min}, Max={Max}, Passes={Passes}",
                    debugTargetUnitId, count, job.MinValueRelationships, job.MaxValueRelationships, passes);
            }
#endif

            umbrellaFilteredContexts = ApplyUmbrellaFilterInMemory(mergedContexts, job);

            logger.LogInformation("Этап фильтрации по числу связей завершён: после Umbrella-фильтра осталось {UnitCount} юнитов", umbrellaFilteredContexts.Count);

#if DEBUG
            var targetAfterUmbrella = umbrellaFilteredContexts.FirstOrDefault(c => c.UnitId == debugTargetUnitId);
            if (targetBeforeUmbrella != null && targetAfterUmbrella == null)
            {
                logger.LogDebug("DEBUG: Юнит {TargetUnitId} ОТФИЛЬТРОВАН на этапе Umbrella", debugTargetUnitId);
            }
#endif
        }

        // Этап 3: Применение takeCount и загрузка полных данных для результата
        var finalContexts = umbrellaFilteredContexts.Take(takeCount ?? int.MaxValue).ToList();

        var loadStopwatch = Stopwatch.StartNew();
        var result = await LoadFinalResultAsync(finalContexts);
        loadStopwatch.Stop();

        totalStopwatch.Stop();
        logger.LogInformation(
            "Job {JobId} завершён за {TotalMs} мс. Найдено {UnitCount} юнитов. [Load: {LoadMs} мс]",
            job.Id,
            totalStopwatch.ElapsedMilliseconds,
            result.Count,
            loadStopwatch.ElapsedMilliseconds
        );

        return result;
    }

    private async Task<Job?> LoadJobWithFiltersAsync(Guid jobId, CancellationToken cancellationToken = default)
    {
        return await jobService
            .Get().AsNoTracking()
            .AsSplitQuery()
            .Include(j => j.UnitFilters).ThenInclude(uf => uf.FieldFilters).ThenInclude(ff => ff.UnitField)
            .Include(j => j.UnitFilters).ThenInclude(uf => uf.RelationshipFilters).ThenInclude(rf => rf.UnitField)
            .Include(j => j.Group).ThenInclude(g => g!.GroupType)
            .FirstOrDefaultAsync(j => j.Id == jobId, cancellationToken);
    }


    /// <summary>
    /// Применяем фильтры аттрибутов
    /// </summary>
    private async Task<List<Guid>> ApplyFieldFiltersOnDbAsync(
        List<Guid> unitIds,
        IEnumerable<JobFieldFilter> fieldFilters,
        CancellationToken cancellationToken = default)
    {
        if (!fieldFilters.Any())
            return unitIds;

        logger.LogDebug("ApplyFieldFiltersOnDbAsync: вход {UnitCount} юнитов, фильтров: {FilterCount}",
            unitIds.Count, fieldFilters.Count());

        var query = unitService.Get().AsNoTracking()
            .Where(u => unitIds.Contains(u.Id));

        int filterIndex = 0;

        foreach (var fieldFilter in fieldFilters)
        {
            filterIndex++;
            var valueMask = fieldFilter.ValueMask?.Trim();
            if (string.IsNullOrEmpty(valueMask))
                continue;

            string dbValueMask = NormalizeLikeMask(valueMask);

            var fieldName = fieldFilter.UnitField?.AihitName ?? $"FieldId={fieldFilter.FieldId}";
            logger.LogDebug("  FieldFilter #{Index}: Поле='{FieldName}', Маска='{Mask}', IsInverse={IsInverse}",
                filterIndex, fieldName, dbValueMask, fieldFilter.IsInverse);

            query = unitService.GetUnitByFieldAndValue(query, fieldFilter.FieldId, fieldFilter.ValueMask!, fieldFilter.IsInverse);

#if DEBUG
            if (unitIds.Contains(debugTargetUnitId))
            {
                var unitHasField = await unitInValueService.Get()
                    .AsNoTracking()
                    .AnyAsync(uiv => uiv.UnitId == debugTargetUnitId && uiv.FieldId == fieldFilter.FieldId, cancellationToken);

                var unitValue = await unitInValueService.Get()
                    .AsNoTracking()
                    .Where(uiv => uiv.UnitId == debugTargetUnitId && uiv.FieldId == fieldFilter.FieldId)
                    .Select(uiv => uiv.Value.Value)
                    .FirstOrDefaultAsync(cancellationToken);

                var intermediateResult = await query.Select(u => u.Id).ToListAsync(cancellationToken);
                var passes = intermediateResult.Contains(debugTargetUnitId);

                logger.LogDebug("  DEBUG: Юнит {TargetUnitId}: Поле={FieldName}, Значение={Value}, Маска={Mask}, HasField={HasField}, Проходит={Passes}",
                    debugTargetUnitId, fieldName, unitValue ?? "null", dbValueMask, unitHasField, passes);

                query = unitService.Get().AsNoTracking()
                    .Where(u => intermediateResult.Contains(u.Id));
            }
#endif
        }

        var result = await query.Select(u => u.Id).ToListAsync(cancellationToken);
        logger.LogDebug("ApplyFieldFiltersOnDbAsync: выход {UnitCount} юнитов", result.Count);

        return result;
    }


    /// <summary>
    /// Применяем фильтры аттрибутов у связанных ЭК
    /// </summary>
    private async Task<List<UnitFilterMatchResult>> ProcessRelationshipFiltersAsync(
        List<Guid> unitIds,
        IEnumerable<JobRelationshipFilter> relationshipFilters,
        CancellationToken cancellationToken = default)
    {
        if (!relationshipFilters.Any() || !unitIds.Any())
        {
            logger.LogDebug("ProcessRelationshipFiltersAsync: вход {UnitCount} юнитов, фильтров: 0 → возврат без изменений",
                unitIds.Count);
            return unitIds.Select(id => new UnitFilterMatchResult { UnitId = id }).ToList();
        }

        logger.LogDebug("ProcessRelationshipFiltersAsync: вход {UnitCount} юнитов, фильтров: {FilterCount}",
            unitIds.Count, relationshipFilters.Count());

#if DEBUG
        if (unitIds.Contains(debugTargetUnitId))
        {
            logger.LogDebug("DEBUG: Юнит {TargetUnitId} найден во входных данных ProcessRelationshipFiltersAsync", debugTargetUnitId);
        }
#endif

        var preFilteredUnitsById = unitIds.ToDictionary(id => id, id => new UnitFilterMatchResult { UnitId = id });

        var parentRelFilters = relationshipFilters.Where(rf => rf.IsParent).ToList();
        var childRelFilters = relationshipFilters.Where(rf => !rf.IsParent).ToList();

        if (parentRelFilters.Any())
        {
            await ApplyRelationshipFiltersToUnitsAsync(
                unitIds,
                preFilteredUnitsById,
                parentRelFilters,
                isParentDirection: true,
                cancellationToken);
        }

        if (childRelFilters.Any())
        {
            await ApplyRelationshipFiltersToUnitsAsync(
                unitIds,
                preFilteredUnitsById,
                childRelFilters,
                isParentDirection: false,
                cancellationToken);
        }

#if DEBUG
        var unitsWithAnyConnections = preFilteredUnitsById.Values.Count(c => c.ValidParentIds.Any() || c.ValidChildIds.Any());
        var unitsWithoutConnections = preFilteredUnitsById.Values.Count(c => !c.ValidParentIds.Any() && !c.ValidChildIds.Any());
        logger.LogDebug("DEBUG: После ApplyRelationshipFiltersToUnitsAsync: {WithConnections} юнитов со связями, {WithoutConnections} без связей",
            unitsWithAnyConnections, unitsWithoutConnections);

        var targetContext = preFilteredUnitsById.Values.FirstOrDefault(c => c.UnitId == debugTargetUnitId);
        if (targetContext != null)
        {
            logger.LogDebug("DEBUG: Юнит {TargetUnitId}: Родителей={ParentCount}, Детей={ChildCount}, PassesFilter={Passes}",
                debugTargetUnitId,
                targetContext.ValidParentIds.Count,
                targetContext.ValidChildIds.Count,
                targetContext.ValidParentIds.Any() || targetContext.ValidChildIds.Any());
        }
#endif

        var result = preFilteredUnitsById.Values.ToList();

        logger.LogDebug("ProcessRelationshipFiltersAsync: выход {ContextCount} контекстов (из {InitialCount})",
            result.Count, preFilteredUnitsById.Count);

#if DEBUG
        if (unitIds.Contains(debugTargetUnitId))
        {
            var targetContextInResult = result.FirstOrDefault(c => c.UnitId == debugTargetUnitId);
            if (targetContextInResult != null)
            {
                logger.LogDebug("DEBUG: Юнит {TargetUnitId} в результатах ProcessRelationshipFiltersAsync. Родителей: {ParentCount}, Детей: {ChildCount}",
                    debugTargetUnitId, targetContextInResult.ValidParentIds.Count, targetContextInResult.ValidChildIds.Count);
            }
            else
            {
                logger.LogDebug("DEBUG: Юнит {TargetUnitId} НЕ в результатах ProcessRelationshipFiltersAsync (удалён)", debugTargetUnitId);
            }
        }

        if (logger.IsEnabled(LogLevel.Debug))
        {
            var allUnitIds = result.Select(c => c.UnitId)
                .Concat(result.SelectMany(c => c.ValidParentIds))
                .Concat(result.SelectMany(c => c.ValidChildIds))
                .Distinct()
                .ToList();

            var unitsMap = await unitService.Get().AsNoTracking()
                .Where(u => allUnitIds.Contains(u.Id))
                .ToDictionaryAsync(u => u.Id, cancellationToken);

            var parentFieldIds = relationshipFilters.Where(rf => rf.IsParent).Select(rf => rf.FieldId).Distinct().ToList();
            var childFieldIds = relationshipFilters.Where(rf => !rf.IsParent).Select(rf => rf.FieldId).Distinct().ToList();
            var allFieldIds = parentFieldIds.Concat(childFieldIds).Distinct().ToList();

            var allValues = await unitInValueService.GetByUnitIdsAndFieldIdsAsync(allUnitIds, allFieldIds);
            var valuesByUnit = allValues.GroupBy(v => v.UnitId)
                .ToDictionary(g => g.Key, g => g.ToList());

            var fieldsMap = await unitFieldService.Get()
                .AsNoTracking()
                .Where(f => allFieldIds.Contains(f.Id))
                .ToDictionaryAsync(f => f.Id, f => f.AihitName, cancellationToken);

            foreach (var context in result.Take(DebugMaxUnitsToLog))
            {
                var u = unitsMap.GetValueOrDefault(context.UnitId);
                if (u == null)
                    continue;

                logger.LogDebug("Найден ЭК {UnitName} (Id={UnitId})", u.Name, u.Id);

                if (context.ValidParentIds.Any())
                {
                    logger.LogDebug("\tФильтрам соответствуют {ParentsCount} родителей:", context.ValidParentIds.Count);

                    foreach (var parentId in context.ValidParentIds.Take(DebugMaxRelationsToLog))
                    {
                        var p = unitsMap.GetValueOrDefault(parentId);
                        logger.LogDebug("\t\t{ParentName} (Id={ParentId})", p?.Name ?? "null", parentId);

                        if (parentFieldIds.Any() && valuesByUnit.TryGetValue(parentId, out var parentValues))
                        {
                            foreach (var pv in parentValues.Where(v => parentFieldIds.Contains(v.FieldId)))
                            {
                                var fieldName = fieldsMap.GetValueOrDefault(pv.FieldId) ?? $"FieldId={pv.FieldId}";
                                logger.LogDebug("\t\t  {FieldName} = {FieldValue}",
                                    fieldName,
                                    pv.Value?.Value ?? "null");
                            }
                        }
                    }

                    if (context.ValidParentIds.Count > DebugMaxRelationsToLog)
                    {
                        logger.LogDebug("\t\t... и ещё {Count} родителей", context.ValidParentIds.Count - DebugMaxRelationsToLog);
                    }
                }
                else if (parentRelFilters.Any())
                {
                    logger.LogDebug("\tРодительские фильтры заданы, но подходящих родителей не найдено");
                }

                if (context.ValidChildIds.Any())
                {
                    logger.LogDebug("\tФильтрам соответствуют {ChildrenCount} детей:", context.ValidChildIds.Count);

                    foreach (var childId in context.ValidChildIds.Take(DebugMaxRelationsToLog))
                    {
                        var c = unitsMap.GetValueOrDefault(childId);
                        logger.LogDebug("\t\t{ChildName} (Id={ChildId})", c?.Name ?? "null", childId);

                        if (childFieldIds.Any() && valuesByUnit.TryGetValue(childId, out var childValues))
                        {
                            foreach (var cv in childValues.Where(v => childFieldIds.Contains(v.FieldId)))
                            {
                                var fieldName = fieldsMap.GetValueOrDefault(cv.FieldId) ?? $"FieldId={cv.FieldId}";
                                logger.LogDebug("\t\t  {FieldName} = {FieldValue}",
                                    fieldName,
                                    cv.Value?.Value ?? "null");
                            }
                        }
                    }

                    if (context.ValidChildIds.Count > DebugMaxRelationsToLog)
                    {
                        logger.LogDebug("\t\t... и ещё {Count} детей", context.ValidChildIds.Count - DebugMaxRelationsToLog);
                    }
                }
                else if (childRelFilters.Any())
                {
                    logger.LogDebug("\tДочерние фильтры заданы, но подходящих детей не найдено");
                }
            }
        }
#endif

        return result;
    }


    /// <summary>
    /// Применяем фильтр по количеству связей для групп работ типа зонтик
    /// </summary>
    /// <param name="contexts"></param>
    /// <param name="job"></param>
    /// <returns></returns>
    private List<UnitFilterMatchResult> ApplyUmbrellaFilterInMemory(List<UnitFilterMatchResult> contexts, Job job)
    {
        var min = job.MinValueRelationships ?? 0;
        var max = job.MaxValueRelationships ?? int.MaxValue;
        var isParentDirection = job.IsParentRelationships == true;

        if (min == 0 && max == int.MaxValue)
            return contexts;

        var result = new List<UnitFilterMatchResult>();
        foreach (var context in contexts)
        {
            int count = isParentDirection ? context.ValidParentIds.Count : context.ValidChildIds.Count;
            bool passes = count >= min && count <= max;

#if DEBUG
            if (context.UnitId == debugTargetUnitId)
            {
                logger.LogDebug("DEBUG Umbrella: Юнит {TargetUnitId}, Count={Count}, Min={Min}, Max={Max}, Passes={Passes}",
                    debugTargetUnitId, count, min, max, passes);
            }
#endif

            if (passes)
            {
                result.Add(context);
            }
        }

        return result;
    }


    /// <summary>
    /// Формируем итоговый результат
    /// </summary>
    /// <param name="contexts"></param>
    /// <returns></returns>
    private async Task<List<UnitFilterResultDto>> LoadFinalResultAsync(List<UnitFilterMatchResult> contexts, CancellationToken cancellationToken = default)
    {
        var allResults = new List<UnitFilterResultDto>();

#if DEBUG
        var targetInContexts = contexts.FirstOrDefault(c => c.UnitId == debugTargetUnitId);
        if (targetInContexts != null)
        {
            logger.LogDebug("DEBUG: Юнит {TargetUnitId} передан в LoadFinalResultAsync. Родителей: {ParentCount}, Детей: {ChildCount}",
                debugTargetUnitId, targetInContexts.ValidParentIds.Count, targetInContexts.ValidChildIds.Count);
        }
        else
        {
            logger.LogDebug("DEBUG: Юнит {TargetUnitId} НЕ передан в LoadFinalResultAsync", debugTargetUnitId);
        }
#endif

        for (int i = 0; i < contexts.Count; i += batchSize)
        {
            var batch = contexts.Skip(i).Take(batchSize).ToList();
            var batchResults = await LoadBatchAsync(batch, cancellationToken);
            allResults.AddRange(batchResults);
        }

        return allResults;
    }


    /// <summary>
    /// Используем пакетную загрузку, чтобы база данных не ругалась
    /// </summary>
    /// <param name="batch"></param>
    /// <returns></returns>
    private async Task<List<UnitFilterResultDto>> LoadBatchAsync(List<UnitFilterMatchResult> batch, CancellationToken cancellationToken = default)
    {
        var unitIds = batch.Select(c => c.UnitId).ToList();
        var allParentIds = batch.SelectMany(c => c.ValidParentIds).Distinct().ToList();
        var allChildIds = batch.SelectMany(c => c.ValidChildIds).Distinct().ToList();

        // Загружаем юниты
        var unitsMap = await unitService.Get().AsNoTracking()
            .AsSingleQuery()
            .Include(u => u.UnitValues).ThenInclude(v => v.Value)
            .Where(u => unitIds.Contains(u.Id))
            .ToDictionaryAsync(u => u.Id, cancellationToken);

        // Загружаем родителей
        Dictionary<Guid, Unit> parentsMap;
        if (allParentIds.Any())
        {
            parentsMap = await unitService.Get().AsNoTracking()
                .AsSingleQuery()
                .Include(u => u.UnitValues).ThenInclude(v => v.Value)
                .Where(u => allParentIds.Contains(u.Id))
                .ToDictionaryAsync(u => u.Id, cancellationToken);
        }
        else
        {
            parentsMap = new Dictionary<Guid, Unit>();
        }

        // Загружаем детей
        Dictionary<Guid, Unit> childrenMap;
        if (allChildIds.Any())
        {
            childrenMap = await unitService.Get().AsNoTracking()
                .AsSingleQuery()
                .Include(u => u.UnitValues).ThenInclude(v => v.Value)
                .Where(u => allChildIds.Contains(u.Id))
                .ToDictionaryAsync(u => u.Id, cancellationToken);
        }
        else
        {
            childrenMap = new Dictionary<Guid, Unit>();
        }

        var result = new List<UnitFilterResultDto>();
        foreach (var context in batch)
        {
            var unit = unitsMap[context.UnitId];
            var dto = new UnitFilterResultDto
            {
                Id = unit.Id,
                Name = unit.Name,
                Values = unit.UnitValues.Select(v => new UnitValueDto
                {
                    FieldId = v.FieldId,
                    Value = v.Value?.Value
                }).ToList(),
                Parents = context.ValidParentIds
                    .Where(id => parentsMap.ContainsKey(id))
                    .Select(id => new RelatedUnitDto
                    {
                        UnitId = id,
                        Name = parentsMap[id].Name,
                        Values = parentsMap[id].UnitValues.Select(v => new UnitValueDto
                        {
                            FieldId = v.FieldId,
                            Value = v.Value?.Value
                        }).ToList()
                    }).ToList(),
                Children = context.ValidChildIds
                    .Where(id => childrenMap.ContainsKey(id))
                    .Select(id => new RelatedUnitDto
                    {
                        UnitId = id,
                        Name = childrenMap[id].Name,
                        Values = childrenMap[id].UnitValues.Select(v => new UnitValueDto
                        {
                            FieldId = v.FieldId,
                            Value = v.Value?.Value
                        }).ToList()
                    }).ToList()
            };
            result.Add(dto);
        }

        return result;
    }


    public async Task<List<string>> GetRelatedUnitNamesAsync(Guid jobId, Guid unitId, CancellationToken cancellationToken = default)
    {
        logger.LogDebug("Начало GetRelatedUnitNamesAsync. JobId: {JobId}, UnitId: {UnitId}", jobId, unitId);

        var job = await jobService
            .Get().AsNoTracking()
            .Include(j => j.UnitFilters).ThenInclude(uf => uf.RelationshipFilters)
            .FirstOrDefaultAsync(j => j.Id == jobId, cancellationToken);

        if (job == null)
        {
            logger.LogWarning("Job с Id {JobId} не найден.", jobId);
            throw new ArgumentException($"Job {jobId} не найден.", nameof(jobId));
        }

        logger.LogDebug("Найден Job: {JobName}. Количество UnitFilters: {FilterCount}", job.Name, job.UnitFilters.Count());

        var result = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

        foreach (var filter in job.UnitFilters)
        {
            if (!filter.RelationshipFilters.Any()) continue;

            logger.LogDebug("Обработка UnitFilter.Id {FilterId}. Количество RelationshipFilters: {RelFilterCount}", filter.Id, filter.RelationshipFilters.Count());

            // Получить все связи для юнита
            var parentLinks = await unitInUnitService.GetByChildIdAsync(unitId);
            var childLinks = await unitInUnitService.GetByParentIdAsync(unitId);

            // Собрать все UnitId, участвующие в связях
            var allRelatedUnitIds = parentLinks
                .Select(l => l.ParentUnitId)
                .Concat(childLinks.Select(l => l.ChildUnitId))
                .Distinct()
                .ToList();

            if (!allRelatedUnitIds.Any()) continue;

            // Получить значения для всех связанных юнитов
            var allUnitValues = await unitInValueService.GetByUnitIdsAsync(allRelatedUnitIds);

            // Сгруппировать значения по UnitId
            var valuesByUnit = allUnitValues
                .GroupBy(uv => uv.UnitId)
                .ToDictionary(g => g.Key, g => g.ToList());

            // Найти UnitId, которые проходят все RelationshipFilters
            var matchingUnitIds = new HashSet<Guid>();

            foreach (var relatedUnitId in allRelatedUnitIds)
            {
                bool passesAllFilters = filter.RelationshipFilters.All(rf =>
                {
                    var values = valuesByUnit.GetValueOrDefault(relatedUnitId, new List<UnitInValue>());

                    var matchingValues = values
                        .Where(uv => uv.FieldId == rf.FieldId && uv.Value?.Value != null)
                        .ToList();

                    if (!matchingValues.Any())
                    {
                        return rf.IsInverse;
                    }

                    var hasMatch = matchingValues.Any(uv => uv.Value!.Value!.Contains(rf.ValueMask.Trim('%'), StringComparison.OrdinalIgnoreCase));

                    if (rf.IsInverse)
                        hasMatch = !hasMatch;

                    return hasMatch;
                });

                if (passesAllFilters)
                    matchingUnitIds.Add(relatedUnitId);
            }

            if (matchingUnitIds.Any())
            {
                var names = await unitService.Get().AsNoTracking()
                    .Where(u => matchingUnitIds.Contains(u.Id))
                    .Select(u => u.Name)
                    .ToListAsync(cancellationToken);
                result.UnionWith(names);
            }
        }

        return result.ToList();
    }


    #region вспомогательные методы


    /// <summary>
    /// Применяет фильтры к связям (родителям или детям)
    /// </summary>
    private async Task ApplyRelationshipFiltersToUnitsAsync(
        List<Guid> unitIds,
        Dictionary<Guid, UnitFilterMatchResult> preFilteredUnitsById,
        List<JobRelationshipFilter> relFilters,
        bool isParentDirection,
        CancellationToken cancellationToken)
    {
        if (!relFilters.Any())
            return;

        var directionName = isParentDirection ? "Родительские" : "Дочерние";
        logger.LogDebug("  {Direction} фильтры ({Count}):", directionName, relFilters.Count);

        // 1. Получить все связи (unitInUnit) для юнитов из unitIds
        var allLinks = await unitInUnitService.Get()
            .AsNoTracking()
            .Where(link => isParentDirection
                ? unitIds.Contains(link.ChildUnitId)
                : unitIds.Contains(link.ParentUnitId))
            .Select(link => new
            {
                SourceId = isParentDirection ? link.ChildUnitId : link.ParentUnitId,
                TargetId = isParentDirection ? link.ParentUnitId : link.ChildUnitId
            })
            .ToListAsync(cancellationToken);

        var allRelatedUnitIds = allLinks.Select(l => l.TargetId).Distinct().ToList();
        logger.LogDebug("    Найдено {TargetCount} уникальных {TargetType} для {LinkCount} связей",
            allRelatedUnitIds.Count,
            isParentDirection ? "родителей" : "детей",
            allLinks.Count);

        // 2. Сгруппировать связи по SourceId (ID юнита из unitIds)
        var relationshipsByUnitId = allLinks.GroupBy(l => l.SourceId)
            .ToDictionary(g => g.Key, g => g.Select(l => l.TargetId).ToList());

        // 3. Для каждого фильтра в группе найдём TargetId, которые ему соответствуют (с учётом IsInverse)
        var matchedTargetIdsByFilter = new Dictionary<JobRelationshipFilter, HashSet<Guid>>();

        foreach (var relFilter in relFilters)
        {
            var valueMask = relFilter.ValueMask?.Trim();
            if (string.IsNullOrWhiteSpace(valueMask))
                continue;

            string dbValueMask = NormalizeLikeMask(valueMask);
            var fieldName = relFilter.UnitField?.AihitName ?? $"FieldId={relFilter.FieldId}";

            logger.LogDebug("    RelationshipFilter ({Direction}): Поле='{FieldName}', Маска='{Mask}', IsInverse={IsInverse}, IsFullMatch={IsFullMatch}",
                isParentDirection ? "Parent" : "Child",
                fieldName, dbValueMask, relFilter.IsInverse, relFilter.IsFullMatch);

            var matchingTargetIds = await unitInValueService.GetMatchingTargetIds(relFilter.FieldId, dbValueMask)
                .ToListAsync(cancellationToken);

            // Ограничиваем найденные ID только теми, которые действительно связаны с юнитами из unitIds
            var filteredMatchingTargetIds = matchingTargetIds
                .Intersect(allRelatedUnitIds)
                .ToList();

            logger.LogDebug("      Найдено {MatchCount} {TargetType} по маске",
                filteredMatchingTargetIds.Count,
                isParentDirection ? "родителей" : "детей");

            HashSet<Guid> targetIdsThatPassThisFilter;

            if (relFilter.IsInverse)
            {
                // Юнит проходит фильтр, если его значение НЕ соответствует маске
                // Это означает, что юниты, НЕ входящие в filteredMatchingTargetIds, проходят фильтр
                targetIdsThatPassThisFilter = new HashSet<Guid>(allRelatedUnitIds.Except(filteredMatchingTargetIds));
                logger.LogDebug("      (IsInverse) Юниты, прошедшие фильтр: {Count}", targetIdsThatPassThisFilter.Count);
            }
            else
            {
                // Юнит проходит фильтр, если его значение соответствует маске
                targetIdsThatPassThisFilter = new HashSet<Guid>(filteredMatchingTargetIds);
                logger.LogDebug("      (Direct) Юниты, прошедшие фильтр: {Count}", targetIdsThatPassThisFilter.Count);
            }

            // Сохраняем результат для этого конкретного фильтра
            matchedTargetIdsByFilter[relFilter] = targetIdsThatPassThisFilter;
        }

        // 4. Определим, какие юниты (context.UnitId) проходят ВСЕ фильтры с учётом IsFullMatch
        var unitIdsToRemove = new HashSet<Guid>();

        foreach (var context in preFilteredUnitsById.Values)
        {
            // Получаем связанные юниты для конкретного context.UnitId
            var relatedUnitIds = relationshipsByUnitId.GetValueOrDefault(context.UnitId, new List<Guid>()).ToHashSet();
            bool hasTargets = relatedUnitIds.Count > 0;

            if (!hasTargets)
            {
                // Нет связей - проверяем, есть ли фильтры, которые требуют наличия связей
                // Если есть хотя бы один фильтр с маской, и связей нет - юнит не проходит.
                var hasFiltersWithMask = relFilters.Any(rf => !string.IsNullOrWhiteSpace(rf.ValueMask?.Trim()));
                if (hasFiltersWithMask)
                {
                    unitIdsToRemove.Add(context.UnitId);
                    continue; // Переходим к следующему юниту
                }
                // Если фильтров с маской нет, юнит остаётся.
            }

            bool unitPassesCurrentFilterGroup = true;

            // Проверяем, проходит ли юнит все фильтры в группе
            foreach (var relFilter in relFilters)
            {
                var valueMask = relFilter.ValueMask?.Trim();
                if (string.IsNullOrWhiteSpace(valueMask))
                {
                    continue; // Пустой фильтр пропускаем
                }

                // Получаем юниты, прошедшие конкретный фильтр relFilter
                var targetIdsThatPassThisFilter = matchedTargetIdsByFilter[relFilter];

                if (relFilter.IsFullMatch)
                {
                    // ВСЕ relatedUnitIds должны пройти этот конкретный фильтр relFilter
                    // Это означает, что каждый relatedUnitId должен быть в targetIdsThatPassThisFilter
                    bool allTargetsPassThisFilter = relatedUnitIds.All(targetId => targetIdsThatPassThisFilter.Contains(targetId));
                    if (!allTargetsPassThisFilter)
                    {
                        unitPassesCurrentFilterGroup = false;
                        break; // Не нужно проверять остальные фильтры, юнит заведомо не проходит
                    }
                }
                else // IsFullMatch = false
                {
                    // Хоть один relatedUnitId должен пройти этот конкретный фильтр relFilter
                    // Это означает, что хотя бы один relatedUnitId должен быть в targetIdsThatPassThisFilter
                    bool anyTargetPassesThisFilter = relatedUnitIds.Any(targetId => targetIdsThatPassThisFilter.Contains(targetId));
                    if (!anyTargetPassesThisFilter)
                    {
                        unitPassesCurrentFilterGroup = false;
                        break; // Не нужно проверять остальные фильтры, юнит заведимо не проходит
                    }
                }
            }

            if (!unitPassesCurrentFilterGroup)
            {
                unitIdsToRemove.Add(context.UnitId);
            }
            else
            {
                // Юнит прошёл все фильтры.
                // Теперь определим, какие из его связей (relatedUnitIds) идут в ValidParentIds/ValidChildIds.
                // Это должны быть связи, прошедшие *все* фильтры (т.е. быть в пересечении результатов для каждого фильтра).
                // Соберём пересечение всех targetId, прошедших каждый фильтр для этого конкретного юнита.
                // Начинаем с множества всех его связей.
                var validRelatedIds = new HashSet<Guid>(relatedUnitIds);

                foreach (var relFilter in relFilters)
                {
                    var valueMask = relFilter.ValueMask?.Trim();
                    if (string.IsNullOrWhiteSpace(valueMask))
                        continue;

                    var targetIdsThatPassThisFilter = matchedTargetIdsByFilter[relFilter];
                    // Пересекаем текущий список validRelatedIds с результатами для этого фильтра
                    validRelatedIds.IntersectWith(targetIdsThatPassThisFilter);
                }

                // Добавляем полученные валидные связи в контекст юнита
                if (isParentDirection)
                {
                    context.ValidParentIds.UnionWith(validRelatedIds);
                }
                else
                {
                    context.ValidChildIds.UnionWith(validRelatedIds);
                }
            }
        }

        // Удаляем юниты, которые не прошли проверку
        foreach (var unitId in unitIdsToRemove)
        {
            preFilteredUnitsById.Remove(unitId);
        }

        logger.LogDebug("    Удалено {RemovedCount} юнитов, не прошедших фильтры (новая логика с IsFullMatch/IsInverse для каждого фильтра)",
            unitIdsToRemove.Count);

        var totalAdded = preFilteredUnitsById.Values.Sum(c =>
            isParentDirection ? c.ValidParentIds.Count : c.ValidChildIds.Count);

        logger.LogDebug("    Добавлено {Total} {TargetType} связей для {UnitCount} юнитов (новая логика)",
            totalAdded,
            isParentDirection ? "родительских" : "дочерних",
            preFilteredUnitsById.Count);
    }


    private async Task<List<Guid>> GetUnitIdsFromCacheOrDbAsync(JobUnitFilter filter, CancellationToken cancellationToken = default)
    {
        // var cacheKey = cacheService.GetKey(new[] { "uf_ids", filter.UnitFilter }, isUseHash: true);
        var cacheKey = cacheService.GetKey(new[] { "unit filter", "unit name mask" }, new[] { filter.UnitFilter });

        var cachedData = await cacheService.GetCachedDataAsync<UnitFilterIds>(cacheKey, true);
        if (cachedData != null)
        {
            return cachedData.Data.UnitIds;
        }

        var dbValueMask = NormalizeLikeMask(filter.UnitFilter);

        var initialUnitIds = await unitService.GetInitialUnitIds(dbValueMask).ToListAsync(cancellationToken);

        var toCache = new UnitFilterIds
        {
            Data = new UnitFilterIdsDto { UnitIds = initialUnitIds },
            Timestamp = DateTimeOffset.UtcNow,
            Source = GetType().Name
        };

        await cacheService.SetCachedDataAsync(cacheKey, toCache, TimeSpan.FromHours(1), true);

        return initialUnitIds;
    }


    /// <summary>
    /// Обрабатывает маску LIKE для корректной работы с SQL
    /// </summary>
    private static string NormalizeLikeMask(string valueMask)
    {
        if (string.IsNullOrWhiteSpace(valueMask))
            return valueMask;

        valueMask = valueMask.Trim();
        bool isStartsWith = valueMask.EndsWith("%") && !valueMask.EndsWith("%%");
        bool isEndsWith = valueMask.StartsWith("%") && !valueMask.StartsWith("%%");

        if (isStartsWith && isEndsWith)
            return $"%{valueMask.Trim('%')}%";
        else if (isStartsWith)
            return $"{valueMask.TrimEnd('%')}%";
        else if (isEndsWith)
            return $"%{valueMask.TrimStart('%')}";
        else
            return valueMask;
    }

    #endregion
}