using LiteCqrs;
using Microsoft.EntityFrameworkCore;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Common.Models;
using TeleWave.Application.Library;

namespace TeleWave.Application.Broadcast.GetSchedule;

public sealed class GetChannelScheduleQueryHandler(IAppDbContext dbContext)
    : IQueryHandler<GetChannelScheduleQuery, Result<IReadOnlyList<ScheduleEntryDto>>>
{
    public async Task<Result<IReadOnlyList<ScheduleEntryDto>>> Handle(
        GetChannelScheduleQuery query,
        CancellationToken cancellationToken
    )
    {
        var channelExists = await dbContext.Channels.AnyAsync(
            c => c.Id == query.ChannelId,
            cancellationToken
        );
        if (!channelExists)
            return Result.Failure<IReadOnlyList<ScheduleEntryDto>>(ChannelErrors.NotFound);

        // Пересекающиеся с окном [from, to) записи.
        var entries = await dbContext
            .ScheduleEntries.AsNoTracking()
            .Where(e =>
                e.ChannelId == query.ChannelId
                && e.StartsAtUtc < query.ToUtc
                && e.EndsAtUtc > query.FromUtc
            )
            .OrderBy(e => e.StartsAtUtc)
            .ToListAsync(cancellationToken);

        var showIds = entries
            .Where(e => e.ShowId != null)
            .Select(e => e.ShowId!.Value)
            .Distinct()
            .ToList();
        var showNames = await dbContext
            .Shows.AsNoTracking()
            .Where(s => showIds.Contains(s.Id))
            .Select(s => new { s.Id, s.Name })
            .ToDictionaryAsync(s => s.Id, s => s.Name, cancellationToken);

        // Подблоки заставок в окне — чтобы показать в расписании, какая именно заставка и с каким текстом.
        var variantIds = entries
            .Where(e =>
                e.Kind == Domain.Broadcast.ScheduleEntryKind.Bumper && e.BumperVariantId != null
            )
            .Select(e => e.BumperVariantId!.Value)
            .Distinct()
            .ToList();
        var variants =
            variantIds.Count == 0
                ? []
                : await dbContext
                    .BumperTextVariants.AsNoTracking()
                    .Where(v => variantIds.Contains(v.Id))
                    .ToListAsync(cancellationToken);
        var variantsById = variants.ToDictionary(v => v.Id);

        // Имена ассетов программ — чтобы показать реальную метку S16E03 в расписании админки.
        var assetIds = entries
            .Where(e => e.Kind == Domain.Broadcast.ScheduleEntryKind.Program)
            .Select(e => e.MediaAssetId)
            .Distinct()
            .ToList();
        var assetNames = await dbContext
            .MediaAssets.AsNoTracking()
            .Where(a => assetIds.Contains(a.Id))
            .Select(a => new { a.Id, a.OriginalFileName })
            .ToDictionaryAsync(a => a.Id, a => a.OriginalFileName, cancellationToken);

        // «Из какого шоу» для заставки берём из ближайшей предыдущей программы в упорядоченном окне.
        Guid? prevProgramShowId = null;
        var dtos = new List<ScheduleEntryDto>(entries.Count);
        foreach (var e in entries)
        {
            string? bumperName = null;
            string? bumperText = null;
            if (
                e.Kind == Domain.Broadcast.ScheduleEntryKind.Bumper
                && e.BumperVariantId is { } vid
                && variantsById.TryGetValue(vid, out var variant)
            )
            {
                bumperName = variant.Name;
                bumperText = BumperText(variant, prevProgramShowId, e.ShowId, showNames);
            }

            dtos.Add(
                new ScheduleEntryDto(
                    e.Id,
                    e.Kind,
                    e.MediaAssetId,
                    e.StartsAtUtc,
                    e.EndsAtUtc,
                    e.ShowId,
                    e.ShowId is { } sid ? showNames.GetValueOrDefault(sid) : null,
                    e.EpisodeIndex,
                    assetNames.TryGetValue(e.MediaAssetId, out var name)
                        ? EpisodeName.ParseLabel(name)
                        : null,
                    bumperName,
                    bumperText
                )
            );

            if (e.Kind == Domain.Broadcast.ScheduleEntryKind.Program)
                prevProgramShowId = e.ShowId;
        }

        return Result.Success<IReadOnlyList<ScheduleEntryDto>>(dtos);
    }

    /// <summary>
    /// Текст заставки для метки в расписании: для «Сейчас/Далее» — подписи + названия шоу (из→в),
    /// для свободного текста — заданные строки. Возвращает null, если показывать нечего.
    /// </summary>
    private static string? BumperText(
        Domain.Broadcast.BumperTextVariant variant,
        Guid? fromShowId,
        Guid? toShowId,
        IReadOnlyDictionary<Guid, string> showNames
    )
    {
        if (variant.Kind == Domain.Broadcast.BumperTextKind.Free)
        {
            var parts = new[] { variant.Line1, variant.Line2 }
                .Where(s => !string.IsNullOrWhiteSpace(s))
                .ToArray();
            return parts.Length == 0 ? null : string.Join(" · ", parts);
        }

        string Name(Guid? id) => id is { } g ? showNames.GetValueOrDefault(g, "…") : "…";
        return $"{variant.NowLabel} {Name(fromShowId)} · {variant.NextLabel} {Name(toShowId)}";
    }
}
