using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using Microsoft.EntityFrameworkCore;
using TeleWave.Application.Broadcast.Bumpers;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Domain.Broadcast;
using TeleWave.Domain.Broadcast.Scheduling;
using TeleWave.Domain.Media;
using TeleWave.Domain.Programming.Planning;

namespace TeleWave.Application.Programming.Planning;

/// <summary>
/// Подставляет ассеты заставкам, которые планировщик зарезервировал. Резерв и рендер разделены
/// намеренно: текст заставки зависит от пары соседей и времени показа, а они известны только после
/// того, как слоты наполнены.
///
/// Готовый ассет переиспользуется по сигнатуре содержимого (оформление блока + подставленный текст),
/// недостающий регистрируется в <see cref="MediaAssetStatus.Pending"/> и уходит фоновому рендереру.
/// Запись при этом ставится в ленту сразу: горизонт — неделя, к эфиру рендер давно закончится.
/// </summary>
public sealed class BumperResolver(
    IAppDbContext dbContext,
    IBumperRenderQueue renderQueue,
    IRandomSource random
)
{
    /// <summary>Ассеты заставок по индексу записи в ленте: одна и та же пара шоу может дать разный текст.</summary>
    public async Task<IReadOnlyDictionary<int, Guid>> ResolveAsync(
        Channel channel,
        IReadOnlyList<PlannedItem> items,
        CancellationToken cancellationToken
    )
    {
        var reserved = items
            .Select((item, index) => (item, index))
            .Where(pair => pair.item.Kind == PlannedItemKind.Bumper)
            .ToList();
        if (reserved.Count == 0)
            return new Dictionary<int, Guid>();

        var templates = await LoadTemplatesAsync(reserved, cancellationToken);
        if (templates.Count == 0)
            return new Dictionary<int, Guid>();

        var tokens = BumperPlaceholders.TokensIn(
            templates
                .Values.SelectMany(t => t.Variants)
                .SelectMany(v => v.Lines)
                .Select(l => l.Text)
        );
        var facts = await BumperFacts.LoadAsync(
            dbContext,
            channel,
            items,
            tokens,
            cancellationToken
        );
        var clipAssets = await LoadClipAssetsAsync(templates.Values, cancellationToken);
        var requests = new List<(int Index, BumperRequest Request)>();

        foreach (var (item, index) in reserved)
        {
            if (
                item.BumperTemplateId is not { } templateId
                || !templates.TryGetValue(templateId, out var template)
            )
                continue;

            var fromShowId = item.FromShowId ?? Guid.Empty;
            var toShowId = item.ToShowId ?? Guid.Empty;
            var variant = PickVariant(template, item.BumperVariantId, fromShowId != toShowId);
            if (variant is null)
                continue;

            var context = facts.Context(item, index);
            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 posterShowId = variant.Background switch
            {
                BumperBackground.NextPoster when toShowId != Guid.Empty => toShowId,
                BumperBackground.NowPoster when fromShowId != Guid.Empty => fromShowId,
                _ => (Guid?)null,
            };

            // Ролик-подложка: нет готового ассета — подблок работает как обычно, по фону блока.
            // Ронять заставку из-за необработанного ролика нельзя, она уже стоит в ленте.
            var clipAssetId =
                variant.Background == BumperBackground.Clip
                && variant.BackgroundClipShowId is { } clipShowId
                && clipAssets.TryGetValue(clipShowId, out var found)
                    ? found
                    : (Guid?)null;

            var linesJson = BumperRenderedText.ToJson(lines);
            requests.Add(
                (
                    index,
                    new BumperRequest(
                        template,
                        variant.Id,
                        linesJson,
                        posterShowId,
                        clipAssetId,
                        ComputeSignature(template, variant.Id, linesJson, posterShowId, clipAssetId)
                    )
                )
            );
        }

        return await MaterializeAsync(requests, cancellationToken);
    }

    /// <summary>Заводит недостающие ассеты и раздаёт готовые по записям ленты.</summary>
    private async Task<IReadOnlyDictionary<int, Guid>> MaterializeAsync(
        IReadOnlyList<(int Index, BumperRequest Request)> requests,
        CancellationToken cancellationToken
    )
    {
        var signatures = requests.Select(r => r.Request.Signature).Distinct().ToList();
        var existing = await dbContext
            .BumperAssets.Where(b => signatures.Contains(b.Signature))
            .ToDictionaryAsync(b => b.Signature, b => b.MediaAssetId, cancellationToken);

        var result = new Dictionary<int, Guid>();
        foreach (var (index, request) in requests)
        {
            if (existing.TryGetValue(request.Signature, out var assetId))
            {
                result[index] = assetId;
                continue;
            }

            var asset = MediaAsset.RegisterGenerated(request.Caption);
            dbContext.MediaAssets.Add(asset);
            dbContext.BumperAssets.Add(
                BumperAsset.Create(
                    request.Template.Id,
                    request.VariantId,
                    request.Signature,
                    request.Lines,
                    request.PosterShowId,
                    asset.Id,
                    request.BackgroundAssetId
                )
            );

            existing[request.Signature] = asset.Id;
            result[index] = asset.Id;
            renderQueue.Enqueue(asset.Id);
        }

        return result;
    }

    private async Task<Dictionary<Guid, BumperTemplate>> LoadTemplatesAsync(
        IReadOnlyList<(PlannedItem Item, int Index)> reserved,
        CancellationToken cancellationToken
    )
    {
        var ids = reserved
            .Select(r => r.Item.BumperTemplateId)
            .Where(id => id is not null)
            .Select(id => id!.Value)
            .Distinct()
            .ToList();

        return await dbContext
            .BumperTemplates.AsNoTracking()
            .Include(t => t.Variants)
            .Where(t => ids.Contains(t.Id))
            .ToDictionaryAsync(t => t.Id, cancellationToken);
    }

    /// <summary>
    /// Подблок: либо жёстко заданный врезкой, либо подходящий под контекст перехода — на смене шоу
    /// и между сериями одного играют разные тексты. Среди подходящих выбор по весам.
    /// </summary>
    private BumperTextVariant? PickVariant(
        BumperTemplate template,
        Guid? fixedVariantId,
        bool isShowChange
    )
    {
        if (fixedVariantId is { } id)
            return template.FindVariant(id);

        var eligible = template
            .Variants.Where(v => v.Matches(isShowChange))
            .OrderBy(v => v.Position)
            .ToList();
        if (eligible.Count == 0)
            return null;

        var total = eligible.Sum(v => Math.Max(0, v.Weight));
        if (total <= 0)
            return eligible[random.Next(eligible.Count)];

        var roll = random.Next(total);
        var accumulated = 0;
        foreach (var variant in eligible)
        {
            accumulated += Math.Max(0, variant.Weight);
            if (roll < accumulated)
                return variant;
        }

        return eligible[^1];
    }

    /// <summary>
    /// Сигнатура — хэш содержимого: оформление блока с его ревизией плюс подставленный текст. Канала
    /// и пары шоу в ней нет намеренно: одинаковая заставка на трёх каналах рендерится один раз, а
    /// <c>{channel}</c> в тексте разводит их сам собой.
    /// </summary>
    private static string ComputeSignature(
        BumperTemplate template,
        Guid variantId,
        string linesJson,
        Guid? posterShowId,
        Guid? backgroundAssetId
    )
    {
        var raw = string.Create(
            CultureInfo.InvariantCulture,
            $"{template.Id}|{template.Revision}|{variantId}|{posterShowId}|{backgroundAssetId}|{linesJson}"
        );
        return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(raw)))[..32];
    }

    /// <summary>
    /// Готовые ассеты роликов-подложек по шоу. Берётся первая готовая серия: у ролика она всегда
    /// одна, а необработанному в эфире взяться неоткуда.
    /// </summary>
    private async Task<Dictionary<Guid, Guid>> LoadClipAssetsAsync(
        IEnumerable<BumperTemplate> templates,
        CancellationToken cancellationToken
    )
    {
        var showIds = templates
            .SelectMany(t => t.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 ready = await dbContext
            .MediaAssets.AsNoTracking()
            .Where(a =>
                a.Status == MediaAssetStatus.Ready
                && a.Duration != null
                && episodes.Select(e => e.MediaAssetId).Contains(a.Id)
            )
            .Select(a => a.Id)
            .ToListAsync(cancellationToken);

        var readySet = ready.ToHashSet();
        return episodes
            .Where(e => readySet.Contains(e.MediaAssetId))
            .GroupBy(e => e.ShowId)
            .ToDictionary(g => g.Key, g => g.First().MediaAssetId);
    }

    /// <summary>Что нужно отрендерить для одной записи ленты.</summary>
    private sealed record BumperRequest(
        BumperTemplate Template,
        Guid VariantId,
        string Lines,
        Guid? PosterShowId,
        Guid? BackgroundAssetId,
        string Signature
    )
    {
        /// <summary>Имя ассета для админки: блок и первые строки заставки.</summary>
        public string Caption
        {
            get
            {
                var text = string.Join(
                    " / ",
                    BumperRenderedText.FromJson(Lines).Select(l => l.Text).Take(2)
                );
                return text.Length == 0 ? Template.Name : $"{Template.Name}: {text}";
            }
        }
    }
}
