using LiteCqrs;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using TeleWave.Application.Broadcast;
using TeleWave.Application.Broadcast.Scheduling;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Common.Models;
using TeleWave.Domain.Broadcast;
using TeleWave.Domain.Programming.Planning;

namespace TeleWave.Application.Programming.Planning.Diff;

public sealed class PreviewApplyDiffQueryHandler(
    IAppDbContext dbContext,
    GridScheduleGenerator generator,
    IOptions<SchedulerOptions> options
) : IQueryHandler<PreviewApplyDiffQuery, Result<ScheduleDiffDto>>
{
    /// <summary>Сколько расхождений отдаём списком: остальное всё равно не читают, а счётчик честный.</summary>
    private const int MaxChanges = 100;

    public async Task<Result<ScheduleDiffDto>> Handle(
        PreviewApplyDiffQuery query,
        CancellationToken cancellationToken
    )
    {
        var now = DateTimeOffset.UtcNow;

        var future = await dbContext
            .ScheduleEntries.AsNoTracking()
            .Where(e => e.ChannelId == query.ChannelId && e.StartsAtUtc >= now)
            .OrderBy(e => e.StartsAtUtc)
            .ToListAsync(cancellationToken);

        // Применение не трогает идущую сейчас запись, поэтому пересборка начинается с её конца —
        // считать иначе значило бы показать диф, которого не будет.
        var currentEnd = await dbContext
            .ScheduleEntries.AsNoTracking()
            .Where(e => e.ChannelId == query.ChannelId && e.StartsAtUtc < now && e.EndsAtUtc > now)
            .MaxAsync(e => (DateTimeOffset?)e.EndsAtUtc, cancellationToken);

        var result = await generator.PreviewAsync(
            query.ChannelId,
            currentEnd ?? now,
            Math.Max(1, options.Value.HorizonDays),
            cancellationToken
        );
        if (result is null)
            return Result.Failure<ScheduleDiffDto>(ChannelErrors.TemplateNotFound);

        var showNames = await LoadShowNamesAsync(future, result.Items, cancellationToken);
        var soonUntil = now.AddHours(24);

        var before = future.Select(e => Describe(e, showNames)).ToList();
        var after = result.Items.Select(i => Describe(i, showNames)).ToList();

        var changes = new List<ScheduleChangeDto>();
        var changed = 0;
        var changedSoon = 0;

        for (var i = 0; i < Math.Max(before.Count, after.Count); i++)
        {
            var oldItem = i < before.Count ? before[i] : null;
            var newItem = i < after.Count ? after[i] : null;
            if (oldItem?.Label == newItem?.Label && oldItem?.StartsAtUtc == newItem?.StartsAtUtc)
                continue;

            var startsAt = newItem?.StartsAtUtc ?? oldItem!.StartsAtUtc;
            var soon = startsAt < soonUntil;

            changed++;
            if (soon)
                changedSoon++;
            if (changes.Count < MaxChanges)
                changes.Add(new ScheduleChangeDto(startsAt, oldItem?.Label, newItem?.Label, soon));
        }

        return Result.Success(new ScheduleDiffDto(future.Count, changed, changedSoon, changes));
    }

    private sealed record Described(DateTimeOffset StartsAtUtc, string Label);

    private static Described Describe(
        ScheduleEntry entry,
        IReadOnlyDictionary<Guid, string> names
    ) =>
        new(
            entry.StartsAtUtc,
            entry.Kind switch
            {
                ScheduleEntryKind.Program => Title(entry.ShowId, entry.EpisodeIndex, names),
                ScheduleEntryKind.Ad => $"реклама {Seconds(entry.EndsAtUtc - entry.StartsAtUtc)}",
                ScheduleEntryKind.Bumper => "заставка",
                ScheduleEntryKind.SignOff => "конец вещания",
                _ => "фон",
            }
        );

    private static Described Describe(PlannedItem item, IReadOnlyDictionary<Guid, string> names) =>
        new(
            item.StartsAtUtc,
            item.Kind switch
            {
                PlannedItemKind.Program => Title(item.ShowId, item.UnitIndex, names),
                PlannedItemKind.Ad or PlannedItemKind.Promo =>
                    $"реклама {Seconds(item.EndsAtUtc - item.StartsAtUtc)}",
                PlannedItemKind.Bumper => "заставка",
                PlannedItemKind.SignOff => "конец вещания",
                _ => "фон",
            }
        );

    private static string Title(
        Guid? showId,
        int? unitIndex,
        IReadOnlyDictionary<Guid, string> names
    )
    {
        var name = showId is { } id && names.TryGetValue(id, out var value) ? value : "—";
        return unitIndex is { } index ? $"{name} · {index + 1}" : name;
    }

    private static string Seconds(TimeSpan duration) => $"{(int)duration.TotalSeconds} с";

    private async Task<Dictionary<Guid, string>> LoadShowNamesAsync(
        IReadOnlyList<ScheduleEntry> future,
        IReadOnlyList<PlannedItem> planned,
        CancellationToken cancellationToken
    )
    {
        var ids = future
            .Select(e => e.ShowId)
            .Concat(planned.Select(i => i.ShowId))
            .Where(id => id is not null && id != Guid.Empty)
            .Select(id => id!.Value)
            .Distinct()
            .ToList();
        if (ids.Count == 0)
            return [];

        return await dbContext
            .Shows.AsNoTracking()
            .Where(s => ids.Contains(s.Id))
            .ToDictionaryAsync(s => s.Id, s => s.Name, cancellationToken);
    }
}
