using TeleWave.Domain.Programming;

namespace TeleWave.Application.Programming.Groups;

/// <summary>Элемент вычисленного состава: чем он там оказался и с какими порядком/весом идёт в эфир.</summary>
/// <param name="Pinned">Закреплён руками (иначе — найден правилом).</param>
public sealed record ResolvedElement(
    GroupElementKind Kind,
    Guid Id,
    int Position,
    int Weight,
    bool Pinned
);

/// <summary>
/// Считает состав группы. Статическая отдаёт свой список как есть; динамическая — то, что нашло
/// правило, плюс закреплённое и минус исключённое.
///
/// Порядок динамической группы: сначала закреплённое (в своём порядке — его задавали руками),
/// затем найденное правилом по алфавиту. Последовательным стратегиям нужен детерминированный
/// порядок, а у найденного правилом своего <c>Position</c> нет и быть не может: позиция живёт
/// на строке таблицы, а строки для него нет. Вес у найденного — единица: взвешивать имеет смысл
/// то, что человек выбрал руками.
/// </summary>
public sealed class DynamicGroupResolver(
    GroupFilterMatcher matcher,
    GroupElementResolver elementResolver
)
{
    public async Task<IReadOnlyList<ResolvedElement>> ResolveAsync(
        Group group,
        CancellationToken cancellationToken
    )
    {
        if (group.Mode != GroupMode.Dynamic)
            return
            [
                .. group
                    .Items.OrderBy(i => i.Position)
                    .Select(i => new ResolvedElement(
                        i.ElementKind,
                        i.ElementId,
                        i.Position,
                        i.Weight,
                        false
                    )),
            ];

        var pinned = group
            .Items.Where(i => i.Role == GroupItemRole.Pinned)
            .OrderBy(i => i.Position)
            .ToList();
        var excluded = group
            .Items.Where(i => i.Role == GroupItemRole.Excluded)
            .Select(i => (i.ElementKind, i.ElementId))
            .ToHashSet();

        var result = new List<ResolvedElement>();
        var position = 0;
        foreach (var item in pinned)
        {
            if (excluded.Contains((item.ElementKind, item.ElementId)))
                continue;
            result.Add(
                new ResolvedElement(item.ElementKind, item.ElementId, position++, item.Weight, true)
            );
        }

        // Без правила динамическая группа сводится к своим закреплённым — это не ошибка, а честное
        // «правило ещё не задано»; пустоту потом покажет постпроверка расписания.
        if (GroupFilter.FromJson(group.FilterJson) is not { } filter)
            return result;

        var taken = result.Select(r => (r.Kind, r.Id)).ToHashSet();
        var matched = await matcher.MatchAsync(filter, cancellationToken);
        var fresh = matched.Where(e => !excluded.Contains(e) && !taken.Contains(e)).ToList();
        if (fresh.Count == 0)
            return result;

        // Разворот нужен и для отсева по длительности единицы, и для алфавитного порядка.
        var info = await elementResolver.ResolveAsync(fresh, cancellationToken);
        var ordered = fresh
            .Where(e =>
                info.TryGetValue(e, out var element)
                && GroupFilterMatcher.MatchesUnitDuration(filter, element)
            )
            .OrderBy(e => info[e].Name, StringComparer.CurrentCultureIgnoreCase)
            .ToList();

        foreach (var (kind, id) in ordered)
            result.Add(new ResolvedElement(kind, id, position++, GroupItem.DefaultWeight, false));

        return result;
    }
}
