namespace TeleWave.Domain.Programming;

/// <summary>
/// Шаблон стыка: что играет между программами. Реклама и заставки перестают быть свойствами канала
/// и становятся врезками стыка — за счёт этого в прайм можно поставить три ролика и заставку,
/// а ночью один длинный ролик, чего одной настройкой на канал не сделать.
///
/// Стык общий для всех каналов, как группа: «рекламный блок на две минуты с заставкой в конце» —
/// такой же переиспользуемый ресурс, как «Боевики 90-х». Канальным остаётся только выбор, какой
/// стык поставить в слот.
/// </summary>
public class JunctionTemplate
{
    private readonly List<JunctionElement> _elements = [];

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

    /// <summary>Потолок длины стыка целиком в секундах; null — ограничен только якорем.</summary>
    public int? MaxTotalSeconds { get; private set; }

    public DateTimeOffset CreatedAt { get; private set; }

    /// <summary>Врезки в порядке показа (backing-field для EF).</summary>
    public IReadOnlyList<JunctionElement> Elements => _elements;

    private JunctionTemplate() { }

    public static JunctionTemplate Create(string name) =>
        new()
        {
            Id = Guid.NewGuid(),
            Name = name.Trim(),
            CreatedAt = DateTimeOffset.UtcNow,
        };

    public void Update(string name, int? maxTotalSeconds)
    {
        Name = name.Trim();
        MaxTotalSeconds = maxTotalSeconds is > 0 ? maxTotalSeconds : null;
    }

    public JunctionElement AddElement(JunctionElementKind kind)
    {
        var nextPosition = _elements.Count == 0 ? 0 : _elements.Max(e => e.Position) + 1;
        var element = JunctionElement.Create(Id, nextPosition, kind);
        _elements.Add(element);
        return element;
    }

    public JunctionElement? FindElement(Guid elementId) =>
        _elements.FirstOrDefault(e => e.Id == elementId);

    public bool RemoveElement(Guid elementId)
    {
        var element = _elements.FirstOrDefault(e => e.Id == elementId);
        if (element is null)
            return false;
        _elements.Remove(element);
        Normalize(_elements.OrderBy(e => e.Position).Select(e => e.Id));
        return true;
    }

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

        Normalize(requested.Concat(rest));
    }

    /// <summary>
    /// Расставляет позиции, стягивая врезки одной развилки в непрерывный отрезок: развилка — это одно
    /// место в цепочке, и её участники, разбросанные по стыку, не имели бы смысла. Каждая приезжает
    /// к первому упоминанию своей метки, поэтому перетащить развилку целиком можно за любого участника.
    /// </summary>
    private void Normalize(IEnumerable<Guid> order)
    {
        var byId = _elements.ToDictionary(e => e.Id);
        var ordered = order.Select(id => byId[id]).ToList();

        var clustered = new List<JunctionElement>();
        var placedChoices = new HashSet<string>(StringComparer.Ordinal);

        foreach (var element in ordered)
        {
            if (element.ChoiceKey is not { } key)
            {
                clustered.Add(element);
                continue;
            }

            if (!placedChoices.Add(key))
                continue;

            clustered.AddRange(ordered.Where(e => e.ChoiceKey == key));
        }

        var position = 0;
        foreach (var element in clustered)
            element.SetPosition(position++);
    }
}
