using TeleWave.Domain.Library;
using TeleWave.Domain.Programming;

namespace TeleWave.Application.Programming.Templates.Generate;

/// <summary>Группа глазами раскладки: чем её можно закрыть, надолго ли хватит и какого она жанра.</summary>
/// <param name="ElementCount">Позиций в составе — по ним считается «повторы чаще раза в неделю».</param>
/// <param name="UnitCount">Единиц воспроизведения: столько эфира группа даёт без повторов.</param>
public sealed record GroupCandidate(
    Guid Id,
    string Name,
    int ElementCount,
    int UnitCount,
    ShowAudience? Strictest,
    ShowKind DominantKind,
    double AverageUnitMinutes,
    Guid? PrimaryGenreId
);

/// <summary>Замечание к плану: <c>IsError</c> — то, что после сборки останется дырой в эфире.</summary>
public sealed record PlanNote(bool IsError, string Text);

/// <summary>
/// Состояние одного разбора: кандидаты, бюджет недели, дневные счётчики и накопленные замечания.
///
/// Бюджет недельный намеренно: слот «каждый день» выходит семь раз, и группа из двенадцати серий
/// закрывает им максимум полторы недели. Считать это внутри суток бессмысленно — там всё всегда
/// сходится, а на неделе не сходится ничего.
/// </summary>
public sealed class PlanRun(
    GridProfile profile,
    IReadOnlyList<GroupCandidate> groups,
    TimeOnly dayStart,
    PlanningRules? rules
)
{
    private readonly Dictionary<Guid, int> _weekUnits = [];
    private readonly Dictionary<Guid, int> _weekAirings = [];
    private Dictionary<Guid, int> _dayUsed = [];
    private Dictionary<Guid, int> _dayGenreMinutes = [];
    private Dictionary<Guid, int> _baselineUsed = [];
    private Dictionary<Guid, int> _baselineGenre = [];
    private int _dayMinutes;
    private int _baselineMinutes;
    private Guid? _lastGenreId;
    private Guid? _lastGroupId;

    private readonly List<PlanNote> _notes = [];
    private readonly HashSet<string> _seen = [];

    public GridProfile Profile { get; } = profile;
    public IReadOnlyList<GroupCandidate> Groups { get; } = groups;
    public TimeOnly DayStart { get; } = dayStart;
    public PlanningRules? Rules { get; } = rules;

    /// <summary>Начало прайма профиля — точка, на которую ссылаются утренние повторы.</summary>
    public TimeOnly? PrimeStart =>
        Profile.Weekdays.FirstOrDefault(b => b.Daypart == Daypart.Prime)?.From;

    public IReadOnlyList<PlanNote> Notes => _notes;

    public void Note(string text, bool isError = false)
    {
        if (_seen.Add(text))
            _notes.Add(new PlanNote(isError, text));
    }

    // ── Сутки ────────────────────────────────────────────────────────────

    /// <summary>Начать сутки. С <paramref name="fromBaseline"/> — продолжить с отложенного среза.</summary>
    public void BeginDay(bool fromBaseline = false)
    {
        _dayUsed = fromBaseline ? new Dictionary<Guid, int>(_baselineUsed) : [];
        _dayGenreMinutes = fromBaseline ? new Dictionary<Guid, int>(_baselineGenre) : [];
        _dayMinutes = fromBaseline ? _baselineMinutes : 0;
        _lastGenreId = null;
        _lastGroupId = null;
    }

    /// <summary>
    /// Запомнить состояние суток. Нужно, когда день строится в два прохода: сначала общие полосы
    /// «каждый день», потом ротация по дням недели — второй проход обязан видеть первый, иначе
    /// вечером встанет то же, что стояло утром.
    /// </summary>
    public void SnapshotDay()
    {
        _baselineUsed = new Dictionary<Guid, int>(_dayUsed);
        _baselineGenre = new Dictionary<Guid, int>(_dayGenreMinutes);
        _baselineMinutes = _dayMinutes;
    }

    public int UsedToday(Guid groupId) => _dayUsed.GetValueOrDefault(groupId);

    /// <summary>Доля суток, уже отданная жанру группы, в процентах.</summary>
    public int GenreShare(GroupCandidate group)
    {
        if (group.PrimaryGenreId is not { } genreId || _dayMinutes == 0)
            return 0;
        return 100 * _dayGenreMinutes.GetValueOrDefault(genreId) / _dayMinutes;
    }

    public bool FollowsSameGenre(GroupCandidate group) =>
        group.PrimaryGenreId is { } genreId && _lastGenreId == genreId;

    /// <summary>
    /// Та же группа, что и в предыдущем слоте. Два блока подряд из одной группы читаются как один
    /// длинный: в сетке это выглядит не ритмом канала, а нехваткой контента.
    /// </summary>
    public bool FollowsSameGroup(GroupCandidate group) => _lastGroupId == group.Id;

    // ── Неделя ───────────────────────────────────────────────────────────

    /// <summary>Единиц группы, ещё не разобранных под слоты этой недели (может уйти в минус).</summary>
    public int RemainingUnits(GroupCandidate group) =>
        group.UnitCount - _weekUnits.GetValueOrDefault(group.Id);

    /// <summary>Ёмкость исчерпана: дальше группа пойдёт по второму кругу.</summary>
    public bool Exhausted(GroupCandidate group) => RemainingUnits(group) <= 0;

    /// <summary>Учесть поставленный слот: единицы недели, выходы, минуты суток и жанр.</summary>
    public void Reserve(GroupCandidate group, int units, int minutes, int airingsPerWeek)
    {
        _weekUnits[group.Id] = _weekUnits.GetValueOrDefault(group.Id) + units * airingsPerWeek;
        _weekAirings[group.Id] = _weekAirings.GetValueOrDefault(group.Id) + airingsPerWeek;
        _dayUsed[group.Id] = _dayUsed.GetValueOrDefault(group.Id) + 1;
        _dayMinutes += minutes;
        _lastGroupId = group.Id;

        if (group.PrimaryGenreId is not { } genreId)
            return;

        _dayGenreMinutes[genreId] = _dayGenreMinutes.GetValueOrDefault(genreId) + minutes;
        _lastGenreId = genreId;
    }

    /// <summary>
    /// Что останется не так после сборки. Считается по тому же бюджету, по которому раскладывалось,
    /// поэтому цифры точные: «84 выхода при 40 позициях» — это ровно то, что потом скажут проверки
    /// сетки, но сказанное до нажатия кнопки.
    /// </summary>
    public IEnumerable<PlanNote> WeeklyChecks()
    {
        foreach (var (groupId, airings) in _weekAirings.OrderBy(p => p.Value))
        {
            var group = Groups.FirstOrDefault(g => g.Id == groupId);
            if (group is null || group.ElementCount >= airings)
                continue;

            yield return new PlanNote(
                false,
                $"В группе «{group.Name}» {group.ElementCount} позиций при {airings} выходах "
                    + "в неделю — повторы пойдут чаще, чем раз в неделю."
            );
        }
    }
}
