using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using Microsoft.EntityFrameworkCore;
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>Ключ отрендеренной заставки: блок, подблок и пара шоу, между которыми она стоит.</summary>
public readonly record struct BumperKey(
    Guid TemplateId,
    Guid VariantId,
    Guid FromShowId,
    Guid ToShowId
);

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

        var showNames = await LoadShowNamesAsync(reserved, cancellationToken);
        var result = new Dictionary<BumperKey, Guid>();

        // Кэш существующих заставок канала: одна пара шоу встречается в горизонте многократно.
        var existing = await dbContext
            .BumperAssets.Where(b => b.ChannelId == channel.Id)
            .ToDictionaryAsync(b => b.Signature, b => b.MediaAssetId, cancellationToken);

        foreach (var item in reserved)
        {
            if (
                item.BumperTemplateId is not { } templateId
                || channel.FindBumperTemplate(templateId) is not { } template
            )
                continue;

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

            var key = new BumperKey(templateId, variant.Id, fromShowId, toShowId);
            if (result.ContainsKey(key))
                continue;

            var fromName = showNames.GetValueOrDefault(fromShowId, "—");
            var toName = showNames.GetValueOrDefault(toShowId, "—");
            var signature = Signature(template, variant.Id, fromName, toName);

            if (existing.TryGetValue(signature, out var assetId))
            {
                result[key] = assetId;
                continue;
            }

            var asset = MediaAsset.RegisterGenerated($"{template.Name}: {fromName} → {toName}");
            dbContext.MediaAssets.Add(asset);
            dbContext.BumperAssets.Add(
                BumperAsset.Create(
                    channel.Id,
                    templateId,
                    variant.Id,
                    fromShowId,
                    toShowId,
                    signature,
                    asset.Id
                )
            );

            existing[signature] = asset.Id;
            result[key] = asset.Id;
            renderQueue.Enqueue(asset.Id);
        }

        return result;
    }

    /// <summary>
    /// Подблок, подходящий под контекст перехода: на смене шоу и между сериями одного играют разные
    /// тексты. Стратегия выбора — общая настройка канала.
    /// </summary>
    private static BumperTextVariant? PickVariant(
        BumperTemplate template,
        bool isShowChange,
        Channel channel,
        IRandomSource random
    )
    {
        var eligible = template
            .Variants.Where(v =>
                v.Trigger switch
                {
                    BumperTrigger.OnShowChange => isShowChange,
                    BumperTrigger.BetweenEpisodes => !isShowChange,
                    _ => true,
                }
            )
            .OrderBy(v => v.Position)
            .ToList();

        if (eligible.Count == 0)
            return null;

        return channel.BumperSelection switch
        {
            BumperSelection.AlwaysFirst => eligible[0],
            BumperSelection.Random => eligible[random.Next(eligible.Count)],
            BumperSelection.WeightedRandom => WeightedPick(eligible, random),
            _ => eligible[random.Next(eligible.Count)],
        };
    }

    private static BumperTextVariant WeightedPick(
        IReadOnlyList<BumperTextVariant> eligible,
        IRandomSource random
    )
    {
        var total = eligible.Sum(v => (long)Math.Max(0, v.Weight));
        if (total <= 0)
            return eligible[random.Next(eligible.Count)];

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

        return eligible[^1];
    }

    /// <summary>
    /// Сигнатура включает версию блока: замена звука или фона обязана пересобрать заставки, иначе
    /// в эфире осталась бы старая картинка с новым оформлением рядом.
    /// </summary>
    private static string Signature(
        BumperTemplate template,
        Guid variantId,
        string fromName,
        string toName
    )
    {
        var raw = string.Create(
            CultureInfo.InvariantCulture,
            $"{template.Id}|{variantId}|{template.Revision}|{fromName}|{toName}"
        );
        return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(raw)))[..32];
    }

    private async Task<Dictionary<Guid, string>> LoadShowNamesAsync(
        IReadOnlyList<PlannedItem> reserved,
        CancellationToken cancellationToken
    )
    {
        var showIds = reserved
            .SelectMany(i => new[] { i.FromShowId, i.ToShowId })
            .Where(id => id is not null && id != Guid.Empty)
            .Select(id => id!.Value)
            .Distinct()
            .ToList();

        if (showIds.Count == 0)
            return [];

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