using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.Constants;
using PARR.DAL.Cache.Services.Base;
using PARR.DAL.Contracts;
using PARR.DAL.DomainModels;
using PARR.DAL.DomainServices.Interfaces;
using PARR.DAL.DomainServices.Shortcodes.Models;
using PARR.DAL.Models;
using PARR.DAL.Models.Job;
using PARR.DAL.Services.Interfaces;
using PARR.DAL.Services.Interfaces.Job;
using PARR.DAL.Services.Interfaces.Unit;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;

namespace PARR.DAL.DomainServices.Shortcodes
{
    /// <summary>
    /// Сервис для подстановки шорткодов в строке на основе данных шаблона.
    /// </summary>
    internal class ShortcodesService : IShortcodesService
    {
        private const string MissingJobWarningMsg =
            "[{Caller}] Шаблон {TemplateId} не содержит Job в переданных данных. Данные будут догружены из БД. " +
            "Рекомендуется обновить запрос шаблона с Include(j => j.Job).";

        private const string MissingIncludesWarningMsg =
            "[{Caller}] Шаблон {TemplateId} содержит Job, но не хватает данных для шорткодов: {Shortcodes}. " +
            "Отсутствуют Include: {MissingIncludes}. Данные будут догружены из БД. " +
            "Рекомендуется обновить запрос шаблона.";

        private const string MissingUnitNameWarningMsg =
            "[{Caller}] Шаблон {TemplateId} не содержит Unit.Name в переданных данных. Данные будут догружены из БД. " +
            "Рекомендуется обновить запрос шаблона с Include(t => t.Unit).";

        private const int MaxIterations = 3;
        private const string NoContent = "Нет данных";
        private static readonly Regex GeneralShortcodeRegex = new(@"%[^%\s]+%", RegexOptions.Compiled);
        private static readonly Regex MaxShortcodeRegex = new(@"%МАКС:([а-яА-Яa-zA-Z0-9_]+)%", RegexOptions.Compiled);
        private static readonly Regex LettersShortcodeRegex = new(@"%БУКВЫ:([^%]+)%", RegexOptions.Compiled);

        private static readonly HashSet<string> SupportedStandardShortcodes = new(StringComparer.OrdinalIgnoreCase)
        {
            "%ЭК%", "%ГРУППА_РАБОТ%", "%РАБОТА%", "%ТНК%", "%ТНК-КРАТКО%", "%ТИКТАК%"
        };

        private readonly ILogger<ShortcodesService> logger;
        private readonly SettingsFromDb settingsFromDb;
        private readonly IJobService jobService;
        private readonly IUnitService unitService;
        private readonly IUnitInValueService unitInValueService;
        private readonly IUnitFieldService unitFieldService;
        private readonly ITemplateService templateService;
        private readonly IRedisCacheService cacheService;
        private readonly IUnitFilterService unitFilterService;

        // Используем record struct вместо class
        private record class TemplateForShortcodes(
            Guid Id,
            int? Index,
            Guid JobId,
            Guid UnitId,
            JobForShortcodes? Job,
            List<UnitInTemplateForShortcodes> UnitsInTemplate
        );

        private record class JobForShortcodes(
            JobGroupForShortcodes? Group,
            TnkForShortcodes? Tnk,
            string WorkName,
            string Name
        );

        private record class JobGroupForShortcodes(
            Guid Id,
            Guid? GroupingUnitFieldId,
            JobGroupTypeForShortcodes? GroupType,
            string GroupName
        );

        private record class JobGroupTypeForShortcodes(
            JobGroupTypesEnum Code
        );

        private record class TnkForShortcodes(
            string Name,
            string ShortName
        );

        private record class UnitInTemplateForShortcodes(
            Guid UnitId
        );

        public ShortcodesService(
            ILogger<ShortcodesService> logger,
            SettingsFromDb settingsFromDb,
            IJobService jobService,
            IUnitService unitService,
            IUnitFilterService unitFilterService,
            IUnitInValueService unitInValueService,
            IUnitFieldService unitFieldService,
            ITemplateService templateService,
            IRedisCacheService cacheService
            )
        {
            this.logger = logger;
            this.settingsFromDb = settingsFromDb;
            this.jobService = jobService;
            this.unitService = unitService;
            this.unitInValueService = unitInValueService;
            this.unitFieldService = unitFieldService;
            this.templateService = templateService;
            this.cacheService = cacheService;
            this.unitFilterService = unitFilterService;
        }

        /// <summary>
        /// Подставляет шорткоды в строке на основе данных шаблона.
        /// </summary>
        /// <param name="str">Строка с шорткодами</param>
        /// <param name="template">Шаблон, из которого берутся данные</param>
        /// <returns>Строка с подставленными значениями</returns>
        public async Task<string> ApplyShortcodesAsync(string str, Template template, [CallerMemberName] string? caller = null)
        {
            if (string.IsNullOrEmpty(str)) return str;
            if (template == null) throw new ArgumentNullException(nameof(template));

            var callerName = caller ?? "Unknown";

            logger.LogDebug("[{Caller}] Начата подстановка шорткодов. Вход: '{Input}', templateId={TemplateId}", callerName, str, template.Id);

            var result = str;
            var iteration = 0;

            while (iteration < MaxIterations)
            {
                var shortcodes = GetShortCodes(result).Select(m => m.Value).ToList();
                if (!shortcodes.Any()) break;

                // Подготавливаем данные с догрузкой при необходимости
                var data = await PrepareTemplateDataAsync(template, shortcodes, callerName).ConfigureAwait(false);

                var oldResult = result;

                // Подстановка всех типов шорткодов
                result = await ApplyAllShortcodesOnceAsync(result, data, shortcodes, callerName).ConfigureAwait(false);

                iteration++;

                if (result == oldResult) break;
            }

            logger.LogDebug("[{Caller}] Подстановка завершена. Результат: '{Result}'", callerName, result);
            return result;
        }

        private async Task<TemplateForShortcodes> PrepareTemplateDataAsync(Template template, List<string> shortcodes, string caller)
        {
            var data = new TemplateForShortcodes(
                Id: template.Id,
                Index: template.Index,
                JobId: template.JobId,
                UnitId: template.UnitId,
                Job: null,
                UnitsInTemplate: template.UnitsInTemplate?.Select(uit => new UnitInTemplateForShortcodes(uit.UnitId)).ToList() ?? new List<UnitInTemplateForShortcodes>()
            );

            // Загружаем Unit.Name
            var unitName = template.Unit?.Name ?? await GetUnitNameAsyncWithWarning(template, shortcodes, caller).ConfigureAwait(false);

            // Проверяем, нужны ли данные Job
            if (!NeedsJobForShortcodes(shortcodes))
            {
                // Всё равно создаём пустой JobForShortcodes, чтобы не было null
                data = data with { Job = new JobForShortcodes(null, null, string.Empty, string.Empty) };
                return data;
            }

            if (template.Job == null)
            {
                logger.LogWarning(MissingJobWarningMsg, caller, template.Id);
                var jobData = await LoadJobForShortcodesAsync(template.JobId, shortcodes, caller).ConfigureAwait(false);
                return data with { Job = jobData };
            }
            else
            {
                var missingIncludes = GetMissingIncludesForShortcodes(shortcodes, template.Job);
                if (missingIncludes.Any())
                {
                    var problematicShortcodes = shortcodes.Where(sc =>
                        (sc.Equals("%ГРУППА_РАБОТ%", StringComparison.OrdinalIgnoreCase) && template.Job.Group == null) ||
                        (sc.StartsWith("%МАКС:", StringComparison.OrdinalIgnoreCase) && template.Job.Group?.GroupType == null) ||
                        (sc.Equals("%ГР_ПОЛЕ-ПН%", StringComparison.OrdinalIgnoreCase) && template.Job.Group?.GroupType == null) ||
                        (sc.Equals("%ИНДЕКС%", StringComparison.OrdinalIgnoreCase) && template.Job.Group?.GroupType == null) ||
                        ((sc.Equals("%ТНК%", StringComparison.OrdinalIgnoreCase) || sc.Equals("%ТНК-КРАТКО%", StringComparison.OrdinalIgnoreCase)) && template.Job.Tnk == null)
                    ).ToList();

                    logger.LogWarning(MissingIncludesWarningMsg, caller, template.Id, string.Join(", ", problematicShortcodes), string.Join(" ", missingIncludes));

                    var jobData = await LoadJobForShortcodesAsync(template.JobId, shortcodes, caller).ConfigureAwait(false);
                    return data with { Job = jobData };
                }
                else
                {
                    var jobData = MapJobForShortcodes(template.Job);
                    return data with { Job = jobData };
                }
            }
        }

        private static bool NeedsJobForShortcodes(List<string> shortcodes)
        {
            return shortcodes.Any(sc =>
                sc != "%ЭК%" && // %ЭК% не требует Job
                (SupportedStandardShortcodes.Contains(sc) ||
                 sc.StartsWith("%МАКС:", StringComparison.OrdinalIgnoreCase) ||
                 sc.Equals("%ГР_ПОЛЕ-ПН%", StringComparison.OrdinalIgnoreCase) ||
                 sc.Equals("%ИНДЕКС%", StringComparison.OrdinalIgnoreCase) ||
                 sc.Equals("%СВЯЗИ%", StringComparison.OrdinalIgnoreCase) ||
                 sc.Equals("%СВЯЗИ-ПН%", StringComparison.OrdinalIgnoreCase)));
        }

        private static List<string> GetMissingIncludesForShortcodes(List<string> shortcodes, Job? job)
        {
            var missing = new List<string>();

            if (shortcodes.Any(sc =>
                    sc.Equals("%ГРУППА_РАБОТ%", StringComparison.OrdinalIgnoreCase) ||
                    sc.StartsWith("%МАКС:", StringComparison.OrdinalIgnoreCase) ||
                    sc.Equals("%ГР_ПОЛЕ-ПН%", StringComparison.OrdinalIgnoreCase) ||
                    sc.Equals("%ИНДЕКС%", StringComparison.OrdinalIgnoreCase))
                && job?.Group == null)
            {
                missing.Add(".Include(j => j.Group)");
            }

            if (shortcodes.Any(sc =>
                    sc.StartsWith("%МАКС:", StringComparison.OrdinalIgnoreCase) ||
                    sc.Equals("%ГР_ПОЛЕ-ПН%", StringComparison.OrdinalIgnoreCase) ||
                    sc.Equals("%ИНДЕКС%", StringComparison.OrdinalIgnoreCase))
                && job?.Group?.GroupType == null)
            {
                missing.Add(".ThenInclude(g => g.GroupType)");
            }

            if (shortcodes.Any(sc =>
                    sc.Equals("%ТНК%", StringComparison.OrdinalIgnoreCase) ||
                    sc.Equals("%ТНК-КРАТКО%", StringComparison.OrdinalIgnoreCase))
                && job?.Tnk == null)
            {
                missing.Add(".Include(j => j.Tnk)");
            }

            return missing;
        }

        private async Task<string> GetUnitNameAsyncWithWarning(Template template, List<string> shortcodes, string caller)
        {
            if (shortcodes.Any(sc => sc.Equals("%ЭК%", StringComparison.OrdinalIgnoreCase)))
            {
                logger.LogWarning(MissingUnitNameWarningMsg, caller, template.Id);
            }

            var unit = await unitService.Get().AsNoTracking().FirstOrDefaultAsync(u => u.Id == template.UnitId).ConfigureAwait(false);
            if (unit == null || string.IsNullOrEmpty(unit.Name))
                throw new InvalidOperationException($"Unit {template.UnitId} не найден или не содержит Name.");

            return unit.Name;
        }

        private JobForShortcodes MapJobForShortcodes(Job job)
        {
            return new JobForShortcodes(
                Group: job.Group == null ? null : new JobGroupForShortcodes(
                    Id: job.Group.Id,
                    GroupingUnitFieldId: job.Group.GroupingUnitFieldId,
                    GroupName: job.Group.GroupName ?? string.Empty,
                    GroupType: job.Group.GroupType == null ? null : new JobGroupTypeForShortcodes(job.Group.GroupType.Code)
                ),
                Tnk: job.Tnk == null ? null : new TnkForShortcodes(
                    Name: job.Tnk.Name ?? string.Empty,
                    ShortName: job.Tnk.ShortName ?? string.Empty
                ),
                WorkName: job.WorkName ?? string.Empty,
                Name: job.Name ?? string.Empty
            );
        }

        private async Task<JobForShortcodes> LoadJobForShortcodesAsync(Guid jobId, List<string> shortcodes, string caller)
        {
            var query = jobService.Get().AsNoTracking();

            // Всегда загружаем Group и GroupType, если нужны групповые шорткоды
            if (shortcodes.Any(sc =>
                sc.StartsWith("%МАКС:", StringComparison.OrdinalIgnoreCase) ||
                sc.Equals("%ГР_ПОЛЕ-ПН%", StringComparison.OrdinalIgnoreCase) ||
                sc.Equals("%ИНДЕКС%", StringComparison.OrdinalIgnoreCase)))
            {
                query = query
                    .Include(j => j.Group)
                        .ThenInclude(g => g!.GroupType);
            }

            // Для стандартных шорткодов тоже может понадобиться Group.GroupName
            if (shortcodes.Any(sc => sc.Equals("%ГРУППА_РАБОТ%", StringComparison.OrdinalIgnoreCase)))
            {
                query = query
                    .Include(j => j.Group);
            }

            // Для Tnk
            if (shortcodes.Any(sc =>
                sc.Equals("%ТНК%", StringComparison.OrdinalIgnoreCase) ||
                sc.Equals("%ТНК-КРАТКО%", StringComparison.OrdinalIgnoreCase)))
            {
                query = query
                    .Include(j => j.Tnk);
            }

            var job = await query.FirstOrDefaultAsync(j => j.Id == jobId).ConfigureAwait(false);
            if (job == null)
                throw new InvalidOperationException($"Job {jobId} не найден.");

            return MapJobForShortcodes(job);
        }

        private async Task<string> ApplyAllShortcodesOnceAsync(string str, TemplateForShortcodes data, List<string> shortcodesInMask, string caller)
        {
            var result = str;

            // 1. Константы
            var nameConstants = settingsFromDb.TemplateNameConstantPartsList;
            if (shortcodesInMask.Any(m => nameConstants.Any(c => $"%{c.Name}%".Equals(m, StringComparison.OrdinalIgnoreCase))))
            {
                result = ReplaceConstants(nameConstants, result);
            }

            // 2. Стандартные
            var hasStandardShortcodes = shortcodesInMask.Any(m => SupportedStandardShortcodes.Contains(m));
            if (hasStandardShortcodes)
            {
                var unitName = await GetUnitNameForTemplateAsync(data.UnitId).ConfigureAwait(false);
                result = ReplaceStandardShortcodes(data.Job, unitName, result);
            }

            // 3. GroupJob: %ИНДЕКС%, %МАКС:..., %ГР_ПОЛЕ-ПН%
            if (shortcodesInMask.Any(m => string.Equals(m, "%ИНДЕКС%", StringComparison.OrdinalIgnoreCase)))
            {
                result = ReplaceSingleShortcode(result, "%ИНДЕКС%", data.Index?.ToString());
            }

            if (data.Job?.Group?.GroupType?.Code == JobGroupTypesEnum.Group)
            {
                var maxShortcodes = MaxShortcodeRegex.Matches(result);
                if (maxShortcodes.Count > 0)
                {
                    result = await ReplaceMaxShortcodesAsync(data.Job.Group.Id, data.UnitId, result, maxShortcodes, caller).ConfigureAwait(false);
                }
            }

            if (shortcodesInMask.Any(m => string.Equals(m, "%ГР_ПОЛЕ-ПН%", StringComparison.OrdinalIgnoreCase)))
            {
                if (data.Job?.Group == null)
                {
                    logger.LogWarning("[{Caller}] Job не содержит Group, необходимый для %ГР_ПОЛЕ-ПН%. Шорткод пропущен.", caller);
                }
                else
                {
                    List<Guid> unitIds;
                    bool wasLoadedFromDb = false;

                    if (data.UnitsInTemplate.Count == 0)
                    {
                        logger.LogWarning(
                            "[{Caller}] Template {TemplateId} не содержит UnitsInTemplate. Данные будут догружены из БД. " +
                            "Рекомендуется обновить запрос с Include(t => t.UnitsInTemplate).",
                            caller, data.Id);

                        var fullTemplate = await templateService.Get()
                            .AsNoTracking()
                            .Include(t => t.UnitsInTemplate)
                            .FirstOrDefaultAsync(t => t.Id == data.Id)
                            .ConfigureAwait(false);

                        unitIds = fullTemplate?.UnitsInTemplate?.Select(uit => uit.UnitId).ToList() ?? new List<Guid>();
                        wasLoadedFromDb = true;
                    }
                    else
                    {
                        unitIds = data.UnitsInTemplate.Select(uit => uit.UnitId).ToList();
                    }

                    if (unitIds.Count == 0)
                    {
                        logger.LogDebug("[{Caller}] Нет UnitsInTemplate для шаблона {TemplateId}. %ГР_ПОЛЕ-ПН% заменён на пустую строку.", caller, data.Id);
                        result = Regex.Replace(result, "%ГР_ПОЛЕ-ПН%", "", RegexOptions.IgnoreCase);
                    }
                    else
                    {
                        var units = await unitService.Get()
                            .AsNoTracking()
                            .Where(u => unitIds.Contains(u.Id))
                            .ToDictionaryAsync(u => u.Id, u => u).ConfigureAwait(false);

                        var groupingFieldId = data.Job.Group.GroupingUnitFieldId;
                        Dictionary<Guid, string> valuesByUnit = new();

                        if (groupingFieldId.HasValue)
                        {
                            var fieldValues = await unitInValueService.Get()
                                .AsNoTracking()
                                .Include(uv => uv.Value)
                                .Where(uv =>
                                    uv.FieldId == groupingFieldId.Value &&
                                    unitIds.Contains(uv.UnitId) &&
                                    uv.Value != null &&
                                    !string.IsNullOrWhiteSpace(uv.Value.Value))
                                .Select(uv => new { uv.UnitId, Value = uv.Value!.Value })
                                .ToListAsync().ConfigureAwait(false);

                            valuesByUnit = fieldValues
                                .GroupBy(x => x.UnitId)
                                .ToDictionary(g => g.Key, g => string.Join(", ", g.OrderBy(v => v.Value).Select(v => v.Value)));
                        }

                        var actualUnitsInTemplate = unitIds.Select(id => new UnitInTemplateForShortcodes(id)).ToList();

                        var sortedUnitsInTemplate = actualUnitsInTemplate
                            .OrderBy(uit => units.TryGetValue(uit.UnitId, out var u) ? u.Name : uit.UnitId.ToString())
                            .ToList();

                        var lines = sortedUnitsInTemplate
                            .Select((uit, i) =>
                            {
                                var unitName = units.TryGetValue(uit.UnitId, out var u) ? u.Name : $"(UnitId={uit.UnitId})";
                                var valuesStr = valuesByUnit.TryGetValue(uit.UnitId, out var vals) ? vals : "";
                                return $"{i + 1}. {unitName} ({valuesStr})";
                            });

                        result = Regex.Replace(result, "%ГР_ПОЛЕ-ПН%", string.Join("\n", lines), RegexOptions.IgnoreCase);
                    }
                }
            }

            // 4. Transform: %БУКВЫ:...
            var lettersShortcodes = LettersShortcodeRegex.Matches(result);
            if (lettersShortcodes.Count > 0)
            {
                result = await ReplaceLettersShortcodesAsync(data.UnitId, result, lettersShortcodes, caller).ConfigureAwait(false);
            }

            // 5. %СВЯЗИ% и %СВЯЗИ-ПН%
            List<string>? relatedUnitNames = null;
            if (shortcodesInMask.Any(m => string.Equals(m, "%СВЯЗИ%", StringComparison.OrdinalIgnoreCase)))
            {
                if (data.Job != null)
                {
                    relatedUnitNames ??= await unitFilterService.GetRelatedUnitNamesAsync(data.JobId, data.UnitId).ConfigureAwait(false);
                    result = Regex.Replace(result, "%СВЯЗИ%", string.Join("\n", relatedUnitNames), RegexOptions.IgnoreCase);
                }
            }
            if (shortcodesInMask.Any(m => string.Equals(m, "%СВЯЗИ-ПН%", StringComparison.OrdinalIgnoreCase)))
            {
                if (data.Job != null)
                {
                    relatedUnitNames ??= await unitFilterService.GetRelatedUnitNamesAsync(data.JobId, data.UnitId).ConfigureAwait(false);
                    var numbered = relatedUnitNames.Select((name, i) => $"{i + 1}. {name}");
                    result = Regex.Replace(result, "%СВЯЗИ-ПН%", string.Join("\n", numbered), RegexOptions.IgnoreCase);
                }
            }

            // 6. Поля
            var fieldShortcodes = GetShortCodes(result);
            if (fieldShortcodes.Count > 0)
                result = await ReplaceFieldValues(data.UnitId, result, fieldShortcodes).ConfigureAwait(false);

            return result;
        }

        private async Task<string> GetUnitNameForTemplateAsync(Guid unitId)
        {
            var unit = await unitService.Get().AsNoTracking().FirstOrDefaultAsync(u => u.Id == unitId).ConfigureAwait(false);
            if (unit == null || string.IsNullOrEmpty(unit.Name))
                throw new InvalidOperationException($"Unit {unitId} не найден или не содержит Name.");
            return unit.Name;
        }

        private static List<Match> GetShortCodes(string resultName)
        {
            var shortcodesInMask = GeneralShortcodeRegex.Matches(resultName).ToList();
            return shortcodesInMask;
        }

        public async Task<List<ShortcodeInfoDto>> GetAvailableShortcodesAsync()
        {
            var result = new List<ShortcodeInfoDto>();

            // 1. Статические константы — из settingsFromDb
            foreach (var constant in settingsFromDb.TemplateNameConstantPartsList)
            {
                result.Add(new ShortcodeInfoDto
                {
                    Shortcode = $"%{constant.Name}%",
                    Description = $"Константа: {constant.Value ?? "(пусто)"}",
                    Type = ShortcodeTypeEnum.Static
                });
            }

            // 2. Стандартные шорткоды
            result.AddRange(new[]
            {
                new ShortcodeInfoDto { Shortcode = "%ЭК%",
                    Description = "Наименование ЭК(Код поиска)",
                    Type = ShortcodeTypeEnum.Standard },
                new ShortcodeInfoDto { Shortcode = "%ГРУППА_РАБОТ%",
                    Description = "Наименование группы работ",
                    Type = ShortcodeTypeEnum.Standard },
                new ShortcodeInfoDto { Shortcode = "%РАБОТА%",
                    Description = "Наименование работы в АСУ ЕСПП",
                    Type = ShortcodeTypeEnum.Standard },
                new ShortcodeInfoDto { Shortcode = "%ТНК%",
                    Description = "Полное наименование ТНК",
                    Type = ShortcodeTypeEnum.Standard },
                new ShortcodeInfoDto { Shortcode = "%ТНК-КРАТКО%",
                    Description = "Краткое наименование ТНК",
                    Type = ShortcodeTypeEnum.Standard },
                new ShortcodeInfoDto { Shortcode = "%ТИКТАК%",
                    Description = "Текущее время в формате Unix timestamp (секунды с 1970-01-01 UTC)",
                    Type = ShortcodeTypeEnum.Standard
                }
            });

            // 3. Групповые шорткоды
            result.AddRange(new[]
            {
                new ShortcodeInfoDto { Shortcode = "%ИНДЕКС%",
                    Description = "Порядковый индекс шаблона для групповых работ",
                    Type = ShortcodeTypeEnum.GroupValue },
                new ShortcodeInfoDto { Shortcode = "%МАКС:ИМЯ АТРИБУТА%",
                    Description = "Используется только с групповым типом работ. Наиболее часто встречающееся значение поля в группе (игнорирует пустые). Пример: %МАКС:РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК%",
                    Type = ShortcodeTypeEnum.GroupValue },
                new ShortcodeInfoDto { Shortcode = "%ГР_ПОЛЕ-ПН%",
                    Description = "Нумерованный список unit-ов из шаблона: 1. ЭК-123 (Значение1, Значение2). Использует GroupingUnitFieldId из JobGroup.",
                    Type = ShortcodeTypeEnum.GroupValue },
            });

            // 4. Трансформирующие шорткоды
            result.Add(new ShortcodeInfoDto
            {
                Shortcode = "%БУКВЫ:ИМЯ АТРИБУТА%",
                Description = "Извлекает только буквы из значения поля. Пример: %БУКВЫ:ЗОНА_ОТВЕТСТВЕННОСТИ% → ПРИВ",
                Type = ShortcodeTypeEnum.Transform
            });

            // 5. Связи
            result.AddRange(new[]
            {
                new ShortcodeInfoDto { Shortcode = "%СВЯЗИ%",
                    Description = "Связанные ЭК (по одному на строку), выбираются только при настроенном фильтре по полям в связанных ЭК",
                    Type = ShortcodeTypeEnum.Relationship },
                new ShortcodeInfoDto { Shortcode = "%СВЯЗИ-ПН%",
                    Description = "Связанные ЭК с нумерацией (1. ..., 2. ...), выбираются только при настроенном фильтре по полям в связанных ЭК",
                    Type = ShortcodeTypeEnum.Relationship },
            });

            // 6. Все доступные поля из UnitField
            var fieldNames = await unitFieldService.Get().AsNoTracking().Select(t => new { t.AihitName, t.DisplayName }).ToListAsync().ConfigureAwait(false);
            foreach (var fieldName in fieldNames.OrderBy(n => n.AihitName))
            {
                result.Add(new ShortcodeInfoDto
                {
                    Shortcode = $"%{fieldName.AihitName}%",
                    Description = $"Атрибут: {fieldName.DisplayName ?? fieldName.AihitName}",
                    Type = ShortcodeTypeEnum.FieldValue
                });
            }
            return result;
        }

        private async Task<string> ReplaceFieldValues(Guid unitId, string resultName, List<Match> shortcodesInMask)
        {
            var requiredFieldNames = shortcodesInMask
                .Select(m => m.Value.Trim('%').ToUpperInvariant())
                .ToList();

            if (requiredFieldNames.Count == 0)
                return resultName;

            var fieldValues = await unitInValueService.GetFieldValuesAsync(unitId, requiredFieldNames).ConfigureAwait(false);

            var fieldValuesMap = fieldValues
                .GroupBy(x => x.FieldName, StringComparer.OrdinalIgnoreCase)
                .ToDictionary(
                    g => g.Key,
                    g => g.Select(x => x.Value).ToList(),
                    StringComparer.OrdinalIgnoreCase);

            foreach (var match in shortcodesInMask)
            {
                var fieldName = match.Value.Trim('%').ToUpperInvariant();

                if (fieldValuesMap.TryGetValue(fieldName, out var values))
                {
                    var combinedValue = string.Join(", ", values.Select(v => v ?? "null"));
                    resultName = resultName.Replace(match.Value, combinedValue);
                }
                else
                {
                    logger.LogWarning("Поле '{FieldName}' не найдено для unitId={UnitId} при подстановке шорткода '{Shortcode}'",
                        fieldName, unitId, match.Value);
                }
            }

            return resultName;
        }

        private static string ReplaceStandardShortcodes(JobForShortcodes? job, string unitName, string input)
        {
            var result = input;

            // %ЭК% — заменяем, только если есть в строке
            if (result.Contains("%ЭК%", StringComparison.OrdinalIgnoreCase))
            {
                result = ReplaceSingleShortcode(result, "%ЭК%", unitName);
            }

            // %ТИКТАК% — не зависит от Job или Unit
            if (result.Contains("%ТИКТАК%", StringComparison.OrdinalIgnoreCase))
            {
                result = ReplaceSingleShortcode(result, "%ТИКТАК%", DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString());
            }

            if (job != null)
            {
                if (result.Contains("%РАБОТА%", StringComparison.OrdinalIgnoreCase))
                {
                    result = ReplaceSingleShortcode(result, "%РАБОТА%", job.WorkName);
                }

                if (result.Contains("%ГРУППА_РАБОТ%", StringComparison.OrdinalIgnoreCase))
                {
                    result = ReplaceSingleShortcode(result, "%ГРУППА_РАБОТ%", job.Group?.GroupName);
                }

                if (result.Contains("%ТНК%", StringComparison.OrdinalIgnoreCase))
                {
                    result = ReplaceSingleShortcode(result, "%ТНК%", job.Tnk?.Name);
                }

                if (result.Contains("%ТНК-КРАТКО%", StringComparison.OrdinalIgnoreCase))
                {
                    result = ReplaceSingleShortcode(result, "%ТНК-КРАТКО%", job.Tnk?.ShortName);
                }
            }

            return result;
        }

        private static string ReplaceSingleShortcode(string input, string shortcode, string? replacement)
        {
            if (replacement == null)
            {
                throw new InvalidOperationException($"Шорткод '{shortcode}' требует непустое значение, но подставляемое значение равно null.");
            }

            return input.Replace(shortcode, replacement, StringComparison.OrdinalIgnoreCase);
        }

        private static string ReplaceConstants(List<BLL.Domain.TemplateNameConstantPart> nameConstants, string resultName)
        {
            foreach (var item in nameConstants)
                resultName = resultName.Replace($"%{item.Name}%", item.Value);

            return resultName;
        }

        private async Task<string> ReplaceMaxShortcodesAsync(Guid jobGroupId, Guid unitId, string input, MatchCollection maxShortcodes, string caller)
        {
            if (jobGroupId == Guid.Empty)
            {
                logger.LogWarning("[{Caller}] jobGroupId не задан. Пропускаем обработку %МАКС:...%", caller);
                return input;
            }

            if (unitId == Guid.Empty)
            {
                logger.LogWarning("[{Caller}] unitId не задан. Пропускаем обработку %МАКС:...%", caller);
                return input;
            }

            var shortcodeToMatches = maxShortcodes
                .Cast<Match>()
                .GroupBy(m => m.Value, StringComparer.OrdinalIgnoreCase)
                .ToDictionary(g => g.Key, g => g.ToList(), StringComparer.OrdinalIgnoreCase);

            foreach (var kvp in shortcodeToMatches)
            {
                var fullShortcode = kvp.Key;
                var matches = kvp.Value;
                var fieldName = fullShortcode.Trim('%').Split(':', 2)[1].Trim();

                logger.LogDebug("[{Caller}] Обработка {Shortcode} для JobGroup {JobGroupId}, Template.UnitId {UnitId}, fieldName {FieldName}",
                    caller, fullShortcode, jobGroupId, unitId, fieldName);

                var mostFrequentValue = await GetMaxShortCodeFromCacheOrDbAsync(jobGroupId, unitId, fullShortcode, fieldName, caller).ConfigureAwait(false);

                foreach (var match in matches)
                    input = input.Replace(match.Value, mostFrequentValue);
            }

            return input;
        }

        private async Task<string> GetMaxShortCodeFromCacheOrDbAsync(Guid jobGroupId, Guid unitId, string fullShortcode, string fieldName, string caller)
        {
            if (jobGroupId == Guid.Empty)
            {
                logger.LogWarning("[{Caller}] jobGroupId не задан. Пропускаем обработку шорткода {Shortcode}", caller, fullShortcode);
                return NoContent;
            }

            if (unitId == Guid.Empty)
            {
                logger.LogWarning("[{Caller}] unitId не задан. Пропускаем обработку шорткода {Shortcode}", caller, fullShortcode);
                return NoContent;
            }

            var cacheKey = $"gr_shcd_{jobGroupId:N}_{unitId:N}_{ComputeHash(fullShortcode)}";

            var cachedData = await cacheService.GetCachedDataAsync<CachedGroupedShortCode>(cacheKey).ConfigureAwait(false);
            if (cachedData != null)
            {
                logger.LogDebug("[{Caller}] Кэш попал для GroupedShortCode '{Name}': {Value}", caller, fullShortcode, cachedData.Value);
                return cachedData.Value;
            }

            logger.LogDebug("[{Caller}] Кэш промахнут для GroupedShortCode '{Name}'. Запрашиваем из БД.", caller, fullShortcode);

            // Загружаем юниты из БД
            var effectiveUnitIds = await jobService.Get()
                .Where(j => j.GroupId == jobGroupId)
                .Join(
                    templateService.Get()
                        .Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Used && t.UnitId == unitId)
                        .Include(t => t.UnitsInTemplate),
                    job => job.Id,
                    template => template.JobId,
                    (job, template) => template
                )
                .SelectMany(template => template.UnitsInTemplate)
                .Select(uit => uit.UnitId)
                .Distinct()
                .ToListAsync().ConfigureAwait(false);

            logger.LogDebug("[{Caller}] EffectiveUnitIds: [{Ids}], Count: {Count}", caller, string.Join(", ", effectiveUnitIds), effectiveUnitIds.Count);

            // Вызываем метод из сервиса
            var mostFrequentValue = await unitInValueService.GetMostFrequentValueForFieldAsync(effectiveUnitIds, fieldName).ConfigureAwait(false);

            logger.LogDebug("[{Caller}] Результат GetMostFrequentValueForFieldAsync: {Result}, для поля {FieldName}, unitIds: [{Ids}]", caller, mostFrequentValue, fieldName, string.Join(", ", effectiveUnitIds));

            // Не кэшируем пустые значения
            if (!string.IsNullOrEmpty(mostFrequentValue))
            {
                var toCache = new CachedGroupedShortCode
                {
                    Value = mostFrequentValue,
                    Timestamp = DateTimeOffset.UtcNow,
                    Source = typeof(ShortcodesService).Name,
                    Version = 1
                };

                await cacheService.SetCachedDataAsync(cacheKey, toCache, TimeSpan.FromHours(1)).ConfigureAwait(false);
                return mostFrequentValue;
            }

            return NoContent;
        }

        private async Task<string> ReplaceLettersShortcodesAsync(Guid unitId, string input, MatchCollection lettersShortcodes, string caller)
        {
            var shortcodeToMatches = lettersShortcodes
                .Cast<Match>()
                .GroupBy(m => m.Value, StringComparer.OrdinalIgnoreCase)
                .ToDictionary(g => g.Key, g => g.ToList(), StringComparer.OrdinalIgnoreCase);

            foreach (var kvp in shortcodeToMatches)
            {
                var fullShortcode = kvp.Key;
                var matches = kvp.Value;
                var fieldName = fullShortcode.Trim('%').Split(':', 2)[1].Trim();

                logger.LogDebug("[{Caller}] Обработка {Shortcode} для unitId {UnitId}, fieldName {FieldName}", caller, fullShortcode, unitId, fieldName);

                var fieldValues = await unitInValueService.GetFieldValuesAsync(unitId, new List<string> { fieldName }).ConfigureAwait(false);

                string extractedLetters = string.Empty;

                if (fieldValues.Any())
                {
                    var value = fieldValues.First().Value;
                    if (value != null)
                    {
                        extractedLetters = ExtractLettersOnly(value);
                        logger.LogDebug("[{Caller}] Извлечены буквы: '{Letters}' из значения '{Value}'", caller, extractedLetters, value);
                    }
                }

                if (string.IsNullOrEmpty(extractedLetters))
                {
                    logger.LogDebug("[{Caller}] Для шорткода {Shortcode} не найдено подходящее значение или из него нельзя извлечь буквы", caller, fullShortcode);
                }

                foreach (var match in matches)
                {
                    input = input.Replace(match.Value, extractedLetters);
                }
            }

            return input;
        }

        private static string ExtractLettersOnly(string input)
        {
            var result = new System.Text.StringBuilder();
            foreach (char c in input)
            {
                if (char.IsLetter(c))
                {
                    result.Append(c);
                }
            }
            return result.ToString();
        }

        private static string ComputeHash(string input)
        {
            using var sha256 = System.Security.Cryptography.SHA256.Create();
            var hashedBytes = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(input));
            return Convert.ToBase64String(hashedBytes).Replace('+', '-').Replace('/', '_').Substring(0, 16);
        }
    }
}