namespace TeleWave.Domain.Programming;

/// <summary>
/// Группа контента — «что может попасть в эфир». Слот планировщика ссылается на группу, стратегия
/// выбирает из неё элемент. Группы общие для всех каналов и переиспользуются.
///
/// Откуда берётся состав, решает <see cref="Mode"/>:
///
/// • <see cref="GroupMode.Static"/> — состав это явный список (<see cref="Items"/>), а
///   <see cref="FilterJson"/> лишь помогает его наполнить: «применить фильтр» добавляет найденное
///   в список, дальше он правится руками.
///
/// • <see cref="GroupMode.Dynamic"/> — состав вычисляет правило в момент обращения, а список хранит
///   только ручные поправки к нему: закреплённое (<see cref="GroupItemRole.Pinned"/>) и исключённое
///   (<see cref="GroupItemRole.Excluded"/>). Само правило домен по-прежнему не интерпретирует —
///   вычисление живёт в Application.
///
/// Уже материализованное расписание в обоих режимах не переписывается: изменение состава проявится
/// на следующем прогоне для ещё не сгенерированных суток. Иначе добавление одного шоу в библиотеку
/// перетасовывало бы будущий эфир всех каналов.
/// </summary>
public class Group
{
    private readonly List<GroupItem> _items = [];

    public Guid Id { get; private set; }
    public string Name { get; private set; } = string.Empty;
    public string? Description { get; private set; }

    /// <summary>Правило набора (JSON) или null. Домен его не интерпретирует — схема живёт в Application.</summary>
    public string? FilterJson { get; private set; }

    /// <summary>Откуда берётся состав: явный список либо вычисление правилом.</summary>
    public GroupMode Mode { get; private set; }

    // ── Кэш статистики: считается при правке состава и фоново, нужен UI («342 позиции · 118 ч») ──

    /// <summary>Число позиций в группе.</summary>
    public int ItemCount { get; private set; }

    /// <summary>Число единиц воспроизведения: у сериала и коллекции их больше одной.</summary>
    public int UnitCount { get; private set; }

    /// <summary>Суммарная длительность готовых единиц.</summary>
    public TimeSpan TotalDuration { get; private set; }

    public DateTimeOffset? StatsComputedAt { get; private set; }

    public DateTimeOffset CreatedAt { get; private set; }

    /// <summary>Позиции группы (backing-field для EF); порядок — по <see cref="GroupItem.Position"/>.</summary>
    public IReadOnlyList<GroupItem> Items => _items;

    private Group() { }

    public static Group Create(string name, string? description = null) =>
        new()
        {
            Id = Guid.NewGuid(),
            Name = name.Trim(),
            Description = description,
            CreatedAt = DateTimeOffset.UtcNow,
        };

    public void Rename(string name, string? description)
    {
        Name = name.Trim();
        Description = description;
    }

    /// <summary>Сохранить правило набора (null — снять). Состав при этом не меняется.</summary>
    public void SetFilter(string? filterJson) =>
        FilterJson = string.IsNullOrWhiteSpace(filterJson) ? null : filterJson;

    /// <summary>
    /// Сменить режим. Переключения не должны менять того, что уже идёт в эфире, поэтому состав
    /// переносится, а не теряется: при переходе в динамический список позиций становится
    /// закреплённым (правило добавит к нему остальное), при обратном переходе закреплённое
    /// становится обычными позициями, а исключения снимаются — исключать больше нечего.
    ///
    /// Вычисленный правилом состав при переходе в статический режим сюда не переносится: домен
    /// правило не интерпретирует. Материализует его вызывающая сторона до смены режима.
    /// </summary>
    public void SetMode(GroupMode mode)
    {
        if (Mode == mode)
            return;

        Mode = mode;
        if (mode == GroupMode.Dynamic)
        {
            foreach (var item in _items)
                item.SetRole(GroupItemRole.Pinned);
            return;
        }

        _items.RemoveAll(i => i.Role == GroupItemRole.Excluded);
        foreach (var item in _items)
            item.SetRole(GroupItemRole.Member);
    }

    /// <summary>Записать пересчитанную статистику.</summary>
    public void UpdateStats(int itemCount, int unitCount, TimeSpan totalDuration, DateTimeOffset at)
    {
        ItemCount = itemCount;
        UnitCount = unitCount;
        TotalDuration = totalDuration;
        StatsComputedAt = at;
    }

    public bool Contains(GroupElementKind kind, Guid elementId) =>
        _items.Any(i => i.ElementKind == kind && i.ElementId == elementId);

    /// <summary>
    /// Добавляет элемент в конец. Повторное добавление игнорируется — возвращает null. Роль по
    /// умолчанию зависит от режима: в динамической группе ручное добавление означает «закрепить»,
    /// иначе правило вымело бы позицию на первом же обращении.
    /// </summary>
    public GroupItem? AddElement(GroupElementKind kind, Guid elementId, GroupItemRole? role = null)
    {
        if (Contains(kind, elementId))
            return null;

        var nextPosition = _items.Count == 0 ? 0 : _items.Max(i => i.Position) + 1;
        var item = GroupItem.Create(Id, kind, elementId, nextPosition, role ?? DefaultRole);
        _items.Add(item);
        return item;
    }

    private GroupItemRole DefaultRole =>
        Mode == GroupMode.Dynamic ? GroupItemRole.Pinned : GroupItemRole.Member;

    /// <summary>
    /// Исключить элемент из динамической группы. Закрепление, если оно было, снимается: держать
    /// на один элемент обе противоположные поправки нельзя. Возвращает false, если группа
    /// статическая — там состав правится списком, а исключать из списка нечего.
    /// </summary>
    public bool Exclude(GroupElementKind kind, Guid elementId)
    {
        if (Mode != GroupMode.Dynamic)
            return false;

        var existing = _items.FirstOrDefault(i =>
            i.ElementKind == kind && i.ElementId == elementId
        );
        if (existing is not null)
        {
            existing.SetRole(GroupItemRole.Excluded);
            return true;
        }

        var nextPosition = _items.Count == 0 ? 0 : _items.Max(i => i.Position) + 1;
        _items.Add(GroupItem.Create(Id, kind, elementId, nextPosition, GroupItemRole.Excluded));
        return true;
    }

    /// <summary>Снять исключение — элемент вернётся в состав, если его находит правило.</summary>
    public bool RemoveExclusion(GroupElementKind kind, Guid elementId) =>
        _items.RemoveAll(i =>
            i.Role == GroupItemRole.Excluded && i.ElementKind == kind && i.ElementId == elementId
        ) > 0;

    public bool RemoveItem(Guid itemId)
    {
        var item = _items.FirstOrDefault(i => i.Id == itemId);
        if (item is null)
            return false;
        _items.Remove(item);
        return true;
    }

    /// <summary>Убирает все позиции, ссылающиеся на элемент. Вызывается при удалении шоу/коллекции
    /// из библиотеки — внешнего ключа на полиморфную ссылку нет. Возвращает число снятых позиций.</summary>
    public int RemoveElement(GroupElementKind kind, Guid elementId) =>
        _items.RemoveAll(i => i.ElementKind == kind && i.ElementId == elementId);

    public bool SetWeight(Guid itemId, int weight)
    {
        var item = _items.FirstOrDefault(i => i.Id == itemId);
        if (item is null)
            return false;
        item.SetWeight(weight);
        return true;
    }

    /// <summary>
    /// Переставляет позиции в порядке переданных идентификаторов. Не упомянутые остаются после них,
    /// сохраняя относительный порядок, а неизвестные игнорируются — так перетаскивание одного
    /// элемента не теряет остальные.
    /// </summary>
    public void Reorder(IEnumerable<Guid> itemIdsInOrder)
    {
        var known = _items.Select(i => i.Id).ToHashSet();
        var requested = itemIdsInOrder.Distinct().Where(known.Contains).ToList();
        var rest = _items
            .Where(i => !requested.Contains(i.Id))
            .OrderBy(i => i.Position)
            .Select(i => i.Id);

        var position = 0;
        foreach (var itemId in requested.Concat(rest))
            _items.First(i => i.Id == itemId).SetPosition(position++);
    }
}
