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

namespace TeleWave.Application.Broadcast.Bumpers;

/// <summary>
/// Рендерит пример каждого подблока. Блок общий, поэтому образцы подстановки берутся глазами
/// выбранного канала: без него <c>{channel}</c> не на что заменить, а названия шоу были бы
/// случайными из библиотеки.
/// </summary>
public sealed class RenderBumperPreviewCommandHandler(
    IAppDbContext dbContext,
    IBumperRenderer renderer,
    IBumperTemplateStorage storage,
    IImageStore imageStore,
    IMediaStorage mediaStorage,
    IOptions<BumperOptions> bumperOptions,
    IOptions<StreamingOptions> streamingOptions
) : ICommandHandler<RenderBumperPreviewCommand, Result>
{
    private readonly BumperOptions _bumper = bumperOptions.Value;
    private readonly int _segmentSeconds = Math.Max(1, streamingOptions.Value.SegmentSeconds);

    public async Task<Result> Handle(
        RenderBumperPreviewCommand command,
        CancellationToken cancellationToken
    )
    {
        var template = await dbContext
            .BumperTemplates.AsNoTracking()
            .Include(t => t.Variants)
            .FirstOrDefaultAsync(t => t.Id == command.TemplateId, cancellationToken);
        if (template is null)
            return Result.Failure(BumperErrors.TemplateNotFound);

        var channel = command.ChannelId is { } channelId
            ? await dbContext
                .Channels.AsNoTracking()
                .FirstOrDefaultAsync(c => c.Id == channelId, cancellationToken)
            : null;

        var samples = await SampleShowsAsync(channel, cancellationToken);
        var context = BuildContext(channel, samples);
        var backgroundPath = await ResolveImagePathAsync(
            template.BackgroundImageId,
            cancellationToken
        );
        var posterPath = await ResolveImagePathAsync(samples.NextPosterId, cancellationToken);
        var aligned = BumperDuration.Aligned(
            BumperDuration.TemplateSeconds(template),
            _segmentSeconds
        );
        var audioPath = storage.AudioPath(template.Id, template.AudioExtension);

        var clips = await ResolveClipsAsync(template, cancellationToken);

        foreach (var variant in template.Variants.OrderBy(v => v.Position))
        {
            var lines = variant
                .Lines.OrderBy(l => l.Position)
                .Select(l => new BumperRenderLine(
                    l.Style,
                    l.Color,
                    BumperPlaceholders.Resolve(l.Text, context)
                ))
                .Where(l => !string.IsNullOrWhiteSpace(l.Text))
                .ToList();

            // У ролика своя длина — в превью она тоже должна быть настоящей, иначе админ увидит
            // не ту заставку, которая пойдёт в эфир.
            var clip = variant.BackgroundClipShowId is { } clipShowId
                ? clips.GetValueOrDefault(clipShowId)
                : null;

            var spec = BumperSpecFactory.Build(
                _bumper,
                template,
                clip is null ? aligned : BumperDuration.Aligned(clip.Seconds, _segmentSeconds),
                new BumperSpecInputs(
                    lines,
                    audioPath,
                    variant.Background == BumperBackground.Template ? null : posterPath,
                    backgroundPath,
                    clip?.Path
                )
            );
            await renderer.RenderAsync(BumperPreview.AssetId(variant.Id), spec, cancellationToken);
        }

        return Result.Success();
    }

    /// <summary>Ролик-подложка: плейлист и длительность.</summary>
    private sealed record ClipSource(string Path, int Seconds);

    /// <summary>Ролики-подложки подблоков блока, по шоу. Неготовые молча выпадают — превью не эфир.</summary>
    private async Task<Dictionary<Guid, ClipSource>> ResolveClipsAsync(
        BumperTemplate template,
        CancellationToken cancellationToken
    )
    {
        var showIds = template
            .Variants.Where(v => v.Background == BumperBackground.Clip)
            .Select(v => v.BackgroundClipShowId)
            .Where(id => id is not null)
            .Select(id => id!.Value)
            .Distinct()
            .ToList();
        if (showIds.Count == 0)
            return [];

        var episodes = await dbContext
            .Shows.AsNoTracking()
            .SelectMany(s => s.Episodes)
            .Where(e => showIds.Contains(e.ShowId))
            .OrderBy(e => e.Position)
            .Select(e => new { e.ShowId, e.MediaAssetId })
            .ToListAsync(cancellationToken);

        var assetIds = episodes.Select(e => e.MediaAssetId).ToList();
        var ready = await dbContext
            .MediaAssets.AsNoTracking()
            .Where(a =>
                assetIds.Contains(a.Id) && a.Status == MediaAssetStatus.Ready && a.Duration != null
            )
            .Select(a => new { a.Id, a.Duration })
            .ToDictionaryAsync(a => a.Id, a => a.Duration!.Value, cancellationToken);

        var result = new Dictionary<Guid, ClipSource>();
        foreach (var episode in episodes)
        {
            if (result.ContainsKey(episode.ShowId))
                continue;
            if (!ready.TryGetValue(episode.MediaAssetId, out var duration))
                continue;
            if (mediaStorage.ResolvePlaylistPath(episode.MediaAssetId) is not { } path)
                continue;

            result[episode.ShowId] = new ClipSource(path, (int)Math.Ceiling(duration.TotalSeconds));
        }

        return result;
    }

    private static BumperContext BuildContext(Channel? channel, SampleShows samples)
    {
        var offset = TimeSpan.FromMinutes(
            channel?.UtcOffsetMinutes ?? Channel.DefaultUtcOffsetMinutes
        );
        var moment = DateTimeOffset.UtcNow.ToOffset(offset);

        return new BumperContext(
            channel?.Name ?? "Канал",
            channel?.Number,
            moment,
            samples.NowTitle,
            samples.NextTitle,
            "с1э5",
            "с2э3",
            samples.NextYear,
            samples.NextGenre,
            TimeOnly.FromDateTime(moment.AddMinutes(30).DateTime),
            samples.SlotTitle,
            // Анонсы в превью показываются образцом: настоящий вечер известен только ленте.
            new BumperAnnouncement(samples.NextTitle, new TimeOnly(20, 0)),
            new BumperAnnouncement(samples.NowTitle, new TimeOnly(20, 0))
        );
    }

    /// <summary>
    /// Пара шоу для образца. Берём те, что реально ходят в этом канале (через группы его слотов), —
    /// иначе предпросмотр показывает библиотеку, а не канал.
    /// </summary>
    private async Task<SampleShows> SampleShowsAsync(
        Channel? channel,
        CancellationToken cancellationToken
    )
    {
        var query = dbContext.Shows.AsNoTracking().AsQueryable();
        if (channel?.TemplateId is { } templateId)
            query =
                from show in query
                join item in dbContext.GroupItems.AsNoTracking() on show.Id equals item.ElementId
                join slot in dbContext.Slots.AsNoTracking() on item.GroupId equals slot.GroupId
                join layer in dbContext.GridLayers.AsNoTracking() on slot.LayerId equals layer.Id
                where layer.TemplateId == templateId && item.ElementKind == GroupElementKind.Show
                select show;

        var shows = await query
            .Select(s => new
            {
                s.Name,
                s.Year,
                s.PosterImageId,
            })
            .Distinct()
            .Take(2)
            .ToListAsync(cancellationToken);

        var slotTitle = channel?.TemplateId is { } id
            ? await (
                from slot in dbContext.Slots.AsNoTracking()
                join layer in dbContext.GridLayers.AsNoTracking() on slot.LayerId equals layer.Id
                where layer.TemplateId == id
                select slot.Title
            ).FirstOrDefaultAsync(cancellationToken)
            : null;

        var next = shows.ElementAtOrDefault(1) ?? shows.ElementAtOrDefault(0);
        return new SampleShows(
            shows.ElementAtOrDefault(0)?.Name ?? "Первое шоу",
            next?.Name ?? "Второе шоу",
            next?.Year,
            null,
            next?.PosterImageId,
            slotTitle ?? "Вечернее кино"
        );
    }

    private async Task<string?> ResolveImagePathAsync(
        Guid? imageId,
        CancellationToken cancellationToken
    )
    {
        if (imageId is not { } id)
            return null;

        var extension = await dbContext
            .Images.AsNoTracking()
            .Where(i => i.Id == id)
            .Select(i => i.FileExtension)
            .FirstOrDefaultAsync(cancellationToken);
        return extension is null ? null : imageStore.ResolvePath(id, extension);
    }

    private sealed record SampleShows(
        string NowTitle,
        string NextTitle,
        int? NextYear,
        string? NextGenre,
        Guid? NextPosterId,
        string SlotTitle
    );
}
